/*
 * Copyright 2017-2026 Espressif Systems (Shanghai) PTE LTD
 * Copyright 2020-2025 Ryan Powell <ryan@nable-embedded.io> and
 * esp-nimble-cpp, NimBLE-Arduino contributors.
 * Copyright 2017 Neil Kolban
 *
 * 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.
 */

/*
 * BLEUtils.cpp
 *
 *  Created on: Mar 25, 2017
 *      Author: kolban
 *
 *  Modified on: Feb 18, 2025
 *      Author: lucasssvaz (based on kolban's and h2zero's work)
 *      Description: Added support for NimBLE
 */

#include "soc/soc_caps.h"
#include "sdkconfig.h"
#if defined(SOC_BLE_SUPPORTED) || defined(CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE)
#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)

/*****************************************************************************
 *                             Common includes                               *
 *****************************************************************************/

#include "BLEAddress.h"
#include "BLEClient.h"
#include "BLEUtils.h"
#include "BLEUUID.h"
#include "GeneralUtils.h"

#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#if SOC_BLE_SUPPORTED
#include <esp_bt.h>
#endif

#include <esp_err.h>
#include <inttypes.h>
#include <map>
#include <sstream>
#include <iomanip>

#include "esp32-hal-log.h"

/*****************************************************************************
 *                            Bluedroid includes                             *
 *****************************************************************************/

#if defined(CONFIG_BLUEDROID_ENABLED)
#include <esp_bt_main.h>
#include <esp_gap_ble_api.h>
#include <esp_gattc_api.h>
#endif

/*****************************************************************************
 *                       NimBLE includes and definitions                     *
 *****************************************************************************/

#if defined(CONFIG_NIMBLE_ENABLED)
#include <host/ble_gap.h>
#include <host/ble_att.h>
#include <host/ble_hs.h>
#include <host/ble_sm.h>
#include <host/ble_l2cap.h>
#include <nimble/hci_common.h>
#include <nimble/ble.h>

#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
#define CONFIG_NIMBLE_ENABLE_RETURN_CODE_TEXT
#define CONFIG_NIMBLE_ENABLE_ADVERTISMENT_TYPE_TEXT
#define CONFIG_NIMBLE_ENABLE_GAP_EVENT_CODE_TEXT
#endif

#ifndef CONFIG_NIMBLE_FREERTOS_TASK_BLOCK_BIT
#define CONFIG_NIMBLE_FREERTOS_TASK_BLOCK_BIT 31
#endif

constexpr uint32_t TASK_BLOCK_BIT = (1 << CONFIG_NIMBLE_FREERTOS_TASK_BLOCK_BIT);
#endif

/*****************************************************************************
 *                       Bluedroid types and constants                       *
 *****************************************************************************/

#if defined(CONFIG_BLUEDROID_ENABLED)
typedef struct {
  uint32_t assignedNumber;
  const char *name;
} member_t;

static const member_t members_ids[] = {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
  {0xFE08, "Microsoft"},
  {0xFE09, "Pillsy, Inc."},
  {0xFE0A, "ruwido austria gmbh"},
  {0xFE0B, "ruwido austria gmbh"},
  {0xFE0C, "Procter & Gamble"},
  {0xFE0D, "Procter & Gamble"},
  {0xFE0E, "Setec Pty Ltd"},
  {0xFE0F, "Philips Lighting B.V."},
  {0xFE10, "Lapis Semiconductor Co., Ltd."},
  {0xFE11, "GMC-I Messtechnik GmbH"},
  {0xFE12, "M-Way Solutions GmbH"},
  {0xFE13, "Apple Inc."},
  {0xFE14, "Flextronics International USA Inc."},
  {0xFE15, "Amazon Fulfillment Services, Inc."},
  {0xFE16, "Footmarks, Inc."},
  {0xFE17, "Telit Wireless Solutions GmbH"},
  {0xFE18, "Runtime, Inc."},
  {0xFE19, "Google Inc."},
  {0xFE1A, "Tyto Life LLC"},
  {0xFE1B, "Tyto Life LLC"},
  {0xFE1C, "NetMedia, Inc."},
  {0xFE1D, "Illuminati Instrument Corporation"},
  {0xFE1E, "Smart Innovations Co., Ltd"},
  {0xFE1F, "Garmin International, Inc."},
  {0xFE20, "Emerson"},
  {0xFE21, "Bose Corporation"},
  {0xFE22, "Zoll Medical Corporation"},
  {0xFE23, "Zoll Medical Corporation"},
  {0xFE24, "August Home Inc"},
  {0xFE25, "Apple, Inc. "},
  {0xFE26, "Google Inc."},
  {0xFE27, "Google Inc."},
  {0xFE28, "Ayla Networks"},
  {0xFE29, "Gibson Innovations"},
  {0xFE2A, "DaisyWorks, Inc."},
  {0xFE2B, "ITT Industries"},
  {0xFE2C, "Google Inc."},
  {0xFE2D, "SMART INNOVATION Co.,Ltd"},
  {0xFE2E, "ERi,Inc."},
  {0xFE2F, "CRESCO Wireless, Inc"},
  {0xFE30, "Volkswagen AG"},
  {0xFE31, "Volkswagen AG"},
  {0xFE32, "Pro-Mark, Inc."},
  {0xFE33, "CHIPOLO d.o.o."},
  {0xFE34, "SmallLoop LLC"},
  {0xFE35, "HUAWEI Technologies Co., Ltd"},
  {0xFE36, "HUAWEI Technologies Co., Ltd"},
  {0xFE37, "Spaceek LTD"},
  {0xFE38, "Spaceek LTD"},
  {0xFE39, "TTS Tooltechnic Systems AG & Co. KG"},
  {0xFE3A, "TTS Tooltechnic Systems AG & Co. KG"},
  {0xFE3B, "Dolby Laboratories"},
  {0xFE3C, "Alibaba"},
  {0xFE3D, "BD Medical"},
  {0xFE3E, "BD Medical"},
  {0xFE3F, "Friday Labs Limited"},
  {0xFE40, "Inugo Systems Limited"},
  {0xFE41, "Inugo Systems Limited"},
  {0xFE42, "Nets A/S "},
  {0xFE43, "Andreas Stihl AG & Co. KG"},
  {0xFE44, "SK Telecom "},
  {0xFE45, "Snapchat Inc"},
  {0xFE46, "B&O Play A/S "},
  {0xFE47, "General Motors"},
  {0xFE48, "General Motors"},
  {0xFE49, "SenionLab AB"},
  {0xFE4A, "OMRON HEALTHCARE Co., Ltd."},
  {0xFE4B, "Philips Lighting B.V."},
  {0xFE4C, "Volkswagen AG"},
  {0xFE4D, "Casambi Technologies Oy"},
  {0xFE4E, "NTT docomo"},
  {0xFE4F, "Molekule, Inc."},
  {0xFE50, "Google Inc."},
  {0xFE51, "SRAM"},
  {0xFE52, "SetPoint Medical"},
  {0xFE53, "3M"},
  {0xFE54, "Motiv, Inc."},
  {0xFE55, "Google Inc."},
  {0xFE56, "Google Inc."},
  {0xFE57, "Dotted Labs"},
  {0xFE58, "Nordic Semiconductor ASA"},
  {0xFE59, "Nordic Semiconductor ASA"},
  {0xFE5A, "Chronologics Corporation"},
  {0xFE5B, "GT-tronics HK Ltd"},
  {0xFE5C, "million hunters GmbH"},
  {0xFE5D, "Grundfos A/S"},
  {0xFE5E, "Plastc Corporation"},
  {0xFE5F, "Eyefi, Inc."},
  {0xFE60, "Lierda Science & Technology Group Co., Ltd."},
  {0xFE61, "Logitech International SA"},
  {0xFE62, "Indagem Tech LLC"},
  {0xFE63, "Connected Yard, Inc."},
  {0xFE64, "Siemens AG"},
  {0xFE65, "CHIPOLO d.o.o."},
  {0xFE66, "Intel Corporation"},
  {0xFE67, "Lab Sensor Solutions"},
  {0xFE68, "Qualcomm Life Inc"},
  {0xFE69, "Qualcomm Life Inc"},
  {0xFE6A, "Kontakt Micro-Location Sp. z o.o."},
  {0xFE6B, "TASER International, Inc."},
  {0xFE6C, "TASER International, Inc."},
  {0xFE6D, "The University of Tokyo"},
  {0xFE6E, "The University of Tokyo"},
  {0xFE6F, "LINE Corporation"},
  {0xFE70, "Beijing Jingdong Century Trading Co., Ltd."},
  {0xFE71, "Plume Design Inc"},
  {0xFE72, "St. Jude Medical, Inc."},
  {0xFE73, "St. Jude Medical, Inc."},
  {0xFE74, "unwire"},
  {0xFE75, "TangoMe"},
  {0xFE76, "TangoMe"},
  {0xFE77, "Hewlett-Packard Company"},
  {0xFE78, "Hewlett-Packard Company"},
  {0xFE79, "Zebra Technologies"},
  {0xFE7A, "Bragi GmbH"},
  {0xFE7B, "Orion Labs, Inc."},
  {0xFE7C, "Telit Wireless Solutions (Formerly Stollmann E+V GmbH)"},
  {0xFE7D, "Aterica Health Inc."},
  {0xFE7E, "Awear Solutions Ltd"},
  {0xFE7F, "Doppler Lab"},
  {0xFE80, "Doppler Lab"},
  {0xFE81, "Medtronic Inc."},
  {0xFE82, "Medtronic Inc."},
  {0xFE83, "Blue Bite"},
  {0xFE84, "RF Digital Corp"},
  {0xFE85, "RF Digital Corp"},
  {0xFE86, "HUAWEI Technologies Co., Ltd. ( )"},
  {0xFE87, "Qingdao Yeelink Information Technology Co., Ltd. ( )"},
  {0xFE88, "SALTO SYSTEMS S.L."},
  {0xFE89, "B&O Play A/S"},
  {0xFE8A, "Apple, Inc."},
  {0xFE8B, "Apple, Inc."},
  {0xFE8C, "TRON Forum"},
  {0xFE8D, "Interaxon Inc."},
  {0xFE8E, "ARM Ltd"},
  {0xFE8F, "CSR"},
  {0xFE90, "JUMA"},
  {0xFE91, "Shanghai Imilab Technology Co.,Ltd"},
  {0xFE92, "Jarden Safety & Security"},
  {0xFE93, "OttoQ Inc."},
  {0xFE94, "OttoQ Inc."},
  {0xFE95, "Xiaomi Inc."},
  {0xFE96, "Tesla Motor Inc."},
  {0xFE97, "Tesla Motor Inc."},
  {0xFE98, "Currant, Inc."},
  {0xFE99, "Currant, Inc."},
  {0xFE9A, "Estimote"},
  {0xFE9B, "Samsara Networks, Inc"},
  {0xFE9C, "GSI Laboratories, Inc."},
  {0xFE9D, "Mobiquity Networks Inc"},
  {0xFE9E, "Dialog Semiconductor B.V."},
  {0xFE9F, "Google Inc."},
  {0xFEA0, "Google Inc."},
  {0xFEA1, "Intrepid Control Systems, Inc."},
  {0xFEA2, "Intrepid Control Systems, Inc."},
  {0xFEA3, "ITT Industries"},
  {0xFEA4, "Paxton Access Ltd"},
  {0xFEA5, "GoPro, Inc."},
  {0xFEA6, "GoPro, Inc."},
  {0xFEA7, "UTC Fire and Security"},
  {0xFEA8, "Savant Systems LLC"},
  {0xFEA9, "Savant Systems LLC"},
  {0xFEAA, "Google Inc."},
  {0xFEAB, "Nokia Corporation"},
  {0xFEAC, "Nokia Corporation"},
  {0xFEAD, "Nokia Corporation"},
  {0xFEAE, "Nokia Corporation"},
  {0xFEAF, "Nest Labs Inc."},
  {0xFEB0, "Nest Labs Inc."},
  {0xFEB1, "Electronics Tomorrow Limited"},
  {0xFEB2, "Microsoft Corporation"},
  {0xFEB3, "Taobao"},
  {0xFEB4, "WiSilica Inc."},
  {0xFEB5, "WiSilica Inc."},
  {0xFEB6, "Vencer Co, Ltd"},
  {0xFEB7, "Facebook, Inc."},
  {0xFEB8, "Facebook, Inc."},
  {0xFEB9, "LG Electronics"},
  {0xFEBA, "Tencent Holdings Limited"},
  {0xFEBB, "adafruit industries"},
  {0xFEBC, "Dexcom, Inc. "},
  {0xFEBD, "Clover Network, Inc."},
  {0xFEBE, "Bose Corporation"},
  {0xFEBF, "Nod, Inc."},
  {0xFEC0, "KDDI Corporation"},
  {0xFEC1, "KDDI Corporation"},
  {0xFEC2, "Blue Spark Technologies, Inc."},
  {0xFEC3, "360fly, Inc."},
  {0xFEC4, "PLUS Location Systems"},
  {0xFEC5, "Realtek Semiconductor Corp."},
  {0xFEC6, "Kocomojo, LLC"},
  {0xFEC7, "Apple, Inc."},
  {0xFEC8, "Apple, Inc."},
  {0xFEC9, "Apple, Inc."},
  {0xFECA, "Apple, Inc."},
  {0xFECB, "Apple, Inc."},
  {0xFECC, "Apple, Inc."},
  {0xFECD, "Apple, Inc."},
  {0xFECE, "Apple, Inc."},
  {0xFECF, "Apple, Inc."},
  {0xFED0, "Apple, Inc."},
  {0xFED1, "Apple, Inc."},
  {0xFED2, "Apple, Inc."},
  {0xFED3, "Apple, Inc."},
  {0xFED4, "Apple, Inc."},
  {0xFED5, "Plantronics Inc."},
  {0xFED6, "Broadcom Corporation"},
  {0xFED7, "Broadcom Corporation"},
  {0xFED8, "Google Inc."},
  {0xFED9, "Pebble Technology Corporation"},
  {0xFEDA, "ISSC Technologies Corporation"},
  {0xFEDB, "Perka, Inc."},
  {0xFEDC, "Jawbone"},
  {0xFEDD, "Jawbone"},
  {0xFEDE, "Coin, Inc."},
  {0xFEDF, "Design SHIFT"},
  {0xFEE0, "Anhui Huami Information Technology Co."},
  {0xFEE1, "Anhui Huami Information Technology Co."},
  {0xFEE2, "Anki, Inc."},
  {0xFEE3, "Anki, Inc."},
  {0xFEE4, "Nordic Semiconductor ASA"},
  {0xFEE5, "Nordic Semiconductor ASA"},
  {0xFEE6, "Silvair, Inc."},
  {0xFEE7, "Tencent Holdings Limited"},
  {0xFEE8, "Quintic Corp."},
  {0xFEE9, "Quintic Corp."},
  {0xFEEA, "Swirl Networks, Inc."},
  {0xFEEB, "Swirl Networks, Inc."},
  {0xFEEC, "Tile, Inc."},
  {0xFEED, "Tile, Inc."},
  {0xFEEE, "Polar Electro Oy"},
  {0xFEEF, "Polar Electro Oy"},
  {0xFEF0, "Intel"},
  {0xFEF1, "CSR"},
  {0xFEF2, "CSR"},
  {0xFEF3, "Google Inc."},
  {0xFEF4, "Google Inc."},
  {0xFEF5, "Dialog Semiconductor GmbH"},
  {0xFEF6, "Wicentric, Inc."},
  {0xFEF7, "Aplix Corporation"},
  {0xFEF8, "Aplix Corporation"},
  {0xFEF9, "PayPal, Inc."},
  {0xFEFA, "PayPal, Inc."},
  {0xFEFB, "Telit Wireless Solutions (Formerly Stollmann E+V GmbH)"},
  {0xFEFC, "Gimbal, Inc."},
  {0xFEFD, "Gimbal, Inc."},
  {0xFEFE, "GN ReSound A/S"},
  {0xFEFF, "GN Netcom"},
  {0xFFFF, "Reserved"}, /*for testing purposes only*/
#endif
  {0, ""}
};

typedef struct {
  uint32_t assignedNumber;
  const char *name;
} gattdescriptor_t;

static const gattdescriptor_t g_descriptor_ids[] = {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
  {0x2905, "Characteristic Aggregate Format"},
  {0x2900, "Characteristic Extended Properties"},
  {0x2904, "Characteristic Presentation Format"},
  {0x2901, "Characteristic User Description"},
  {0x2902, "Client Characteristic Configuration"},
  {0x290B, "Environmental Sensing Configuration"},
  {0x290C, "Environmental Sensing Measurement"},
  {0x290D, "Environmental Sensing Trigger Setting"},
  {0x2907, "External Report Reference"},
  {0x2909, "Number of Digitals"},
  {0x2908, "Report Reference"},
  {0x2903, "Server Characteristic Configuration"},
  {0x290E, "Time Trigger Setting"},
  {0x2906, "Valid Range"},
  {0x290A, "Value Trigger Setting"},
#endif
  {0, ""}
};

typedef struct {
  uint32_t assignedNumber;
  const char *name;
} characteristicMap_t;

static const characteristicMap_t g_characteristicsMappings[] = {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
  {0x2A7E, "Aerobic Heart Rate Lower Limit"},
  {0x2A84, "Aerobic Heart Rate Upper Limit"},
  {0x2A7F, "Aerobic Threshold"},
  {0x2A80, "Age"},
  {0x2A5A, "Aggregate"},
  {0x2A43, "Alert Category ID"},
  {0x2A42, "Alert Category ID Bit Mask"},
  {0x2A06, "Alert Level"},
  {0x2A44, "Alert Notification Control Point"},
  {0x2A3F, "Alert Status"},
  {0x2AB3, "Altitude"},
  {0x2A81, "Anaerobic Heart Rate Lower Limit"},
  {0x2A82, "Anaerobic Heart Rate Upper Limit"},
  {0x2A83, "Anaerobic Threshold"},
  {0x2A58, "Analog"},
  {0x2A59, "Analog Output"},
  {0x2A73, "Apparent Wind Direction"},
  {0x2A72, "Apparent Wind Speed"},
  {0x2A01, "Appearance"},
  {0x2AA3, "Barometric Pressure Trend"},
  {0x2A19, "Battery Level"},
  {0x2A1B, "Battery Level State"},
  {0x2A1A, "Battery Power State"},
  {0x2A49, "Blood Pressure Feature"},
  {0x2A35, "Blood Pressure Measurement"},
  {0x2A9B, "Body Composition Feature"},
  {0x2A9C, "Body Composition Measurement"},
  {0x2A38, "Body Sensor Location"},
  {0x2AA4, "Bond Management Control Point"},
  {0x2AA5, "Bond Management Features"},
  {0x2A22, "Boot Keyboard Input Report"},
  {0x2A32, "Boot Keyboard Output Report"},
  {0x2A33, "Boot Mouse Input Report"},
  {0x2AA6, "Central Address Resolution"},
  {0x2AA8, "CGM Feature"},
  {0x2AA7, "CGM Measurement"},
  {0x2AAB, "CGM Session Run Time"},
  {0x2AAA, "CGM Session Start Time"},
  {0x2AAC, "CGM Specific Ops Control Point"},
  {0x2AA9, "CGM Status"},
  {0x2ACE, "Cross Trainer Data"},
  {0x2A5C, "CSC Feature"},
  {0x2A5B, "CSC Measurement"},
  {0x2A2B, "Current Time"},
  {0x2A66, "Cycling Power Control Point"},
  {0x2A66, "Cycling Power Control Point"},
  {0x2A65, "Cycling Power Feature"},
  {0x2A65, "Cycling Power Feature"},
  {0x2A63, "Cycling Power Measurement"},
  {0x2A64, "Cycling Power Vector"},
  {0x2A99, "Database Change Increment"},
  {0x2A85, "Date of Birth"},
  {0x2A86, "Date of Threshold Assessment"},
  {0x2A08, "Date Time"},
  {0x2A0A, "Day Date Time"},
  {0x2A09, "Day of Week"},
  {0x2A7D, "Descriptor Value Changed"},
  {0x2A00, "Device Name"},
  {0x2A7B, "Dew Point"},
  {0x2A56, "Digital"},
  {0x2A57, "Digital Output"},
  {0x2A0D, "DST Offset"},
  {0x2A6C, "Elevation"},
  {0x2A87, "Email Address"},
  {0x2A0B, "Exact Time 100"},
  {0x2A0C, "Exact Time 256"},
  {0x2A88, "Fat Burn Heart Rate Lower Limit"},
  {0x2A89, "Fat Burn Heart Rate Upper Limit"},
  {0x2A26, "Firmware Revision String"},
  {0x2A8A, "First Name"},
  {0x2AD9, "Fitness Machine Control Point"},
  {0x2ACC, "Fitness Machine Feature"},
  {0x2ADA, "Fitness Machine Status"},
  {0x2A8B, "Five Zone Heart Rate Limits"},
  {0x2AB2, "Floor Number"},
  {0x2A8C, "Gender"},
  {0x2A51, "Glucose Feature"},
  {0x2A18, "Glucose Measurement"},
  {0x2A34, "Glucose Measurement Context"},
  {0x2A74, "Gust Factor"},
  {0x2A27, "Hardware Revision String"},
  {0x2A39, "Heart Rate Control Point"},
  {0x2A8D, "Heart Rate Max"},
  {0x2A37, "Heart Rate Measurement"},
  {0x2A7A, "Heat Index"},
  {0x2A8E, "Height"},
  {0x2A4C, "HID Control Point"},
  {0x2A4A, "HID Information"},
  {0x2A8F, "Hip Circumference"},
  {0x2ABA, "HTTP Control Point"},
  {0x2AB9, "HTTP Entity Body"},
  {0x2AB7, "HTTP Headers"},
  {0x2AB8, "HTTP Status Code"},
  {0x2ABB, "HTTPS Security"},
  {0x2A6F, "Humidity"},
  {0x2A2A, "IEEE 11073-20601 Regulatory Certification Data List"},
  {0x2AD2, "Indoor Bike Data"},
  {0x2AAD, "Indoor Positioning Configuration"},
  {0x2A36, "Intermediate Cuff Pressure"},
  {0x2A1E, "Intermediate Temperature"},
  {0x2A77, "Irradiance"},
  {0x2AA2, "Language"},
  {0x2A90, "Last Name"},
  {0x2AAE, "Latitude"},
  {0x2A6B, "LN Control Point"},
  {0x2A6A, "LN Feature"},
  {0x2AB1, "Local East Coordinate"},
  {0x2AB0, "Local North Coordinate"},
  {0x2A0F, "Local Time Information"},
  {0x2A67, "Location and Speed Characteristic"},
  {0x2AB5, "Location Name"},
  {0x2AAF, "Longitude"},
  {0x2A2C, "Magnetic Declination"},
  {0x2AA0, "Magnetic Flux Density - 2D"},
  {0x2AA1, "Magnetic Flux Density - 3D"},
  {0x2A29, "Manufacturer Name String"},
  {0x2A91, "Maximum Recommended Heart Rate"},
  {0x2A21, "Measurement Interval"},
  {0x2A24, "Model Number String"},
  {0x2A68, "Navigation"},
  {0x2A3E, "Network Availability"},
  {0x2A46, "New Alert"},
  {0x2AC5, "Object Action Control Point"},
  {0x2AC8, "Object Changed"},
  {0x2AC1, "Object First-Created"},
  {0x2AC3, "Object ID"},
  {0x2AC2, "Object Last-Modified"},
  {0x2AC6, "Object List Control Point"},
  {0x2AC7, "Object List Filter"},
  {0x2ABE, "Object Name"},
  {0x2AC4, "Object Properties"},
  {0x2AC0, "Object Size"},
  {0x2ABF, "Object Type"},
  {0x2ABD, "OTS Feature"},
  {0x2A04, "Peripheral Preferred Connection Parameters"},
  {0x2A02, "Peripheral Privacy Flag"},
  {0x2A5F, "PLX Continuous Measurement Characteristic"},
  {0x2A60, "PLX Features"},
  {0x2A5E, "PLX Spot-Check Measurement"},
  {0x2A50, "PnP ID"},
  {0x2A75, "Pollen Concentration"},
  {0x2A2F, "Position 2D"},
  {0x2A30, "Position 3D"},
  {0x2A69, "Position Quality"},
  {0x2A6D, "Pressure"},
  {0x2A4E, "Protocol Mode"},
  {0x2A62, "Pulse Oximetry Control Point"},
  {0x2A60, "Pulse Oximetry Pulsatile Event Characteristic"},
  {0x2A78, "Rainfall"},
  {0x2A03, "Reconnection Address"},
  {0x2A52, "Record Access Control Point"},
  {0x2A14, "Reference Time Information"},
  {0x2A3A, "Removable"},
  {0x2A4D, "Report"},
  {0x2A4B, "Report Map"},
  {0x2AC9, "Resolvable Private Address Only"},
  {0x2A92, "Resting Heart Rate"},
  {0x2A40, "Ringer Control point"},
  {0x2A41, "Ringer Setting"},
  {0x2AD1, "Rower Data"},
  {0x2A54, "RSC Feature"},
  {0x2A53, "RSC Measurement"},
  {0x2A55, "SC Control Point"},
  {0x2A4F, "Scan Interval Window"},
  {0x2A31, "Scan Refresh"},
  {0x2A3C, "Scientific Temperature Celsius"},
  {0x2A10, "Secondary Time Zone"},
  {0x2A5D, "Sensor Location"},
  {0x2A25, "Serial Number String"},
  {0x2A05, "Service Changed"},
  {0x2A3B, "Service Required"},
  {0x2A28, "Software Revision String"},
  {0x2A93, "Sport Type for Aerobic and Anaerobic Thresholds"},
  {0x2AD0, "Stair Climber Data"},
  {0x2ACF, "Step Climber Data"},
  {0x2A3D, "String"},
  {0x2AD7, "Supported Heart Rate Range"},
  {0x2AD5, "Supported Inclination Range"},
  {0x2A47, "Supported New Alert Category"},
  {0x2AD8, "Supported Power Range"},
  {0x2AD6, "Supported Resistance Level Range"},
  {0x2AD4, "Supported Speed Range"},
  {0x2A48, "Supported Unread Alert Category"},
  {0x2A23, "System ID"},
  {0x2ABC, "TDS Control Point"},
  {0x2A6E, "Temperature"},
  {0x2A1F, "Temperature Celsius"},
  {0x2A20, "Temperature Fahrenheit"},
  {0x2A1C, "Temperature Measurement"},
  {0x2A1D, "Temperature Type"},
  {0x2A94, "Three Zone Heart Rate Limits"},
  {0x2A12, "Time Accuracy"},
  {0x2A15, "Time Broadcast"},
  {0x2A13, "Time Source"},
  {0x2A16, "Time Update Control Point"},
  {0x2A17, "Time Update State"},
  {0x2A11, "Time with DST"},
  {0x2A0E, "Time Zone"},
  {0x2AD3, "Training Status"},
  {0x2ACD, "Treadmill Data"},
  {0x2A71, "True Wind Direction"},
  {0x2A70, "True Wind Speed"},
  {0x2A95, "Two Zone Heart Rate Limit"},
  {0x2A07, "Tx Power Level"},
  {0x2AB4, "Uncertainty"},
  {0x2A45, "Unread Alert Status"},
  {0x2AB6, "URI"},
  {0x2A9F, "User Control Point"},
  {0x2A9A, "User Index"},
  {0x2A76, "UV Index"},
  {0x2A96, "VO2 Max"},
  {0x2A97, "Waist Circumference"},
  {0x2A98, "Weight"},
  {0x2A9D, "Weight Measurement"},
  {0x2A9E, "Weight Scale Feature"},
  {0x2A79, "Wind Chill"},
#endif
  {0, ""}
};

/**
 * @brief Mapping from service ids to names
 */
typedef struct {
  const char *name;
  const char *type;
  uint32_t assignedNumber;
} gattService_t;

/**
 * Definition of the service ids to names that we know about.
 */
static const gattService_t g_gattServices[] = {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
  {"Alert Notification Service", "org.bluetooth.service.alert_notification", 0x1811},
  {"Automation IO", "org.bluetooth.service.automation_io", 0x1815},
  {"Battery Service", "org.bluetooth.service.battery_service", 0x180F},
  {"Blood Pressure", "org.bluetooth.service.blood_pressure", 0x1810},
  {"Body Composition", "org.bluetooth.service.body_composition", 0x181B},
  {"Bond Management", "org.bluetooth.service.bond_management", 0x181E},
  {"Continuous Glucose Monitoring", "org.bluetooth.service.continuous_glucose_monitoring", 0x181F},
  {"Current Time Service", "org.bluetooth.service.current_time", 0x1805},
  {"Cycling Power", "org.bluetooth.service.cycling_power", 0x1818},
  {"Cycling Speed and Cadence", "org.bluetooth.service.cycling_speed_and_cadence", 0x1816},
  {"Device Information", "org.bluetooth.service.device_information", 0x180A},
  {"Environmental Sensing", "org.bluetooth.service.environmental_sensing", 0x181A},
  {"Generic Access", "org.bluetooth.service.generic_access", 0x1800},
  {"Generic Attribute", "org.bluetooth.service.generic_attribute", 0x1801},
  {"Glucose", "org.bluetooth.service.glucose", 0x1808},
  {"Health Thermometer", "org.bluetooth.service.health_thermometer", 0x1809},
  {"Heart Rate", "org.bluetooth.service.heart_rate", 0x180D},
  {"HTTP Proxy", "org.bluetooth.service.http_proxy", 0x1823},
  {"Human Interface Device", "org.bluetooth.service.human_interface_device", 0x1812},
  {"Immediate Alert", "org.bluetooth.service.immediate_alert", 0x1802},
  {"Indoor Positioning", "org.bluetooth.service.indoor_positioning", 0x1821},
  {"Internet Protocol Support", "org.bluetooth.service.internet_protocol_support", 0x1820},
  {"Link Loss", "org.bluetooth.service.link_loss", 0x1803},
  {"Location and Navigation", "org.bluetooth.service.location_and_navigation", 0x1819},
  {"Next DST Change Service", "org.bluetooth.service.next_dst_change", 0x1807},
  {"Object Transfer", "org.bluetooth.service.object_transfer", 0x1825},
  {"Phone Alert Status Service", "org.bluetooth.service.phone_alert_status", 0x180E},
  {"Pulse Oximeter", "org.bluetooth.service.pulse_oximeter", 0x1822},
  {"Reference Time Update Service", "org.bluetooth.service.reference_time_update", 0x1806},
  {"Running Speed and Cadence", "org.bluetooth.service.running_speed_and_cadence", 0x1814},
  {"Scan Parameters", "org.bluetooth.service.scan_parameters", 0x1813},
  {"Transport Discovery", "org.bluetooth.service.transport_discovery", 0x1824},
  {"Tx Power", "org.bluetooth.service.tx_power", 0x1804},
  {"User Data", "org.bluetooth.service.user_data", 0x181C},
  {"Weight Scale", "org.bluetooth.service.weight_scale", 0x181D},
#endif
  {"", "", 0}
};
#endif

/*****************************************************************************
 *                             Common functions                              *
 *****************************************************************************/

/**
 * @brief Create a hex representation of data.
 *
 * @param [in] target Where to write the hex string.  If this is null, we malloc storage.
 * @param [in] source The start of the binary data.
 * @param [in] length The length of the data to convert.
 * @return A pointer to the formatted buffer.
 */
char *BLEUtils::buildHexData(uint8_t *target, const uint8_t *source, uint8_t length) {
  // Guard against too much data.
  if (length > 100) {
    length = 100;
  }

  if (target == nullptr) {
    target = (uint8_t *)malloc(length * 2 + 1);
    if (target == nullptr) {
      log_e("buildHexData: malloc failed");
      return nullptr;
    }
  }
  char *startOfData = (char *)target;

  for (int i = 0; i < length; i++) {
    snprintf((char *)target, 3, "%02x", source[i]);
    target += 2;
  }

  // Handle the special case where there was no data.
  if (length == 0) {
    *startOfData = 0;
  }

  return startOfData;
}  // buildHexData

/**
 * @brief Given an advertising data type, return a string representation of the type.
 *
 * For details see ...
 * https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile
 *
 * @return A string representation of the type.
 */
const char *BLEUtils::advDataTypeToString(uint8_t advType) {
  switch (advType) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    case ESP_BLE_AD_TYPE_FLAG:  // 0x01
      return "ESP_BLE_AD_TYPE_FLAG";
    case ESP_BLE_AD_TYPE_16SRV_PART:  // 0x02
      return "ESP_BLE_AD_TYPE_16SRV_PART";
    case ESP_BLE_AD_TYPE_16SRV_CMPL:  // 0x03
      return "ESP_BLE_AD_TYPE_16SRV_CMPL";
    case ESP_BLE_AD_TYPE_32SRV_PART:  // 0x04
      return "ESP_BLE_AD_TYPE_32SRV_PART";
    case ESP_BLE_AD_TYPE_32SRV_CMPL:  // 0x05
      return "ESP_BLE_AD_TYPE_32SRV_CMPL";
    case ESP_BLE_AD_TYPE_128SRV_PART:  // 0x06
      return "ESP_BLE_AD_TYPE_128SRV_PART";
    case ESP_BLE_AD_TYPE_128SRV_CMPL:  // 0x07
      return "ESP_BLE_AD_TYPE_128SRV_CMPL";
    case ESP_BLE_AD_TYPE_NAME_SHORT:  // 0x08
      return "ESP_BLE_AD_TYPE_NAME_SHORT";
    case ESP_BLE_AD_TYPE_NAME_CMPL:  // 0x09
      return "ESP_BLE_AD_TYPE_NAME_CMPL";
    case ESP_BLE_AD_TYPE_TX_PWR:  // 0x0a
      return "ESP_BLE_AD_TYPE_TX_PWR";
    case ESP_BLE_AD_TYPE_DEV_CLASS:  // 0x0b
      return "ESP_BLE_AD_TYPE_DEV_CLASS";
    case ESP_BLE_AD_TYPE_SM_TK:  // 0x10
      return "ESP_BLE_AD_TYPE_SM_TK";
    case ESP_BLE_AD_TYPE_SM_OOB_FLAG:  // 0x11
      return "ESP_BLE_AD_TYPE_SM_OOB_FLAG";
    case ESP_BLE_AD_TYPE_INT_RANGE:  // 0x12
      return "ESP_BLE_AD_TYPE_INT_RANGE";
    case ESP_BLE_AD_TYPE_SOL_SRV_UUID:  // 0x14
      return "ESP_BLE_AD_TYPE_SOL_SRV_UUID";
    case ESP_BLE_AD_TYPE_128SOL_SRV_UUID:  // 0x15
      return "ESP_BLE_AD_TYPE_128SOL_SRV_UUID";
    case ESP_BLE_AD_TYPE_SERVICE_DATA:  // 0x16
      return "ESP_BLE_AD_TYPE_SERVICE_DATA";
    case ESP_BLE_AD_TYPE_PUBLIC_TARGET:  // 0x17
      return "ESP_BLE_AD_TYPE_PUBLIC_TARGET";
    case ESP_BLE_AD_TYPE_RANDOM_TARGET:  // 0x18
      return "ESP_BLE_AD_TYPE_RANDOM_TARGET";
    case ESP_BLE_AD_TYPE_APPEARANCE:  // 0x19
      return "ESP_BLE_AD_TYPE_APPEARANCE";
    case ESP_BLE_AD_TYPE_ADV_INT:  // 0x1a
      return "ESP_BLE_AD_TYPE_ADV_INT";
    case ESP_BLE_AD_TYPE_32SOL_SRV_UUID:  // 0x1f
      return "ESP_BLE_AD_TYPE_32SOL_SRV_UUID";
    case ESP_BLE_AD_TYPE_32SERVICE_DATA:  // 0x20
      return "ESP_BLE_AD_TYPE_32SERVICE_DATA";
    case ESP_BLE_AD_TYPE_128SERVICE_DATA:  // 0x21
      return "ESP_BLE_AD_TYPE_128SERVICE_DATA";
    case ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE:  // 0xff
      return "ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE";
#endif
    default: log_v(" adv data type: 0x%x", advType); return "";
  }  // End switch
}  // advDataTypeToString

/*****************************************************************************
 *                            Bluedroid functions                            *
 *****************************************************************************/

#if defined(CONFIG_BLUEDROID_ENABLED)

/**
 * @brief Convert characteristic properties into a string representation.
 * @param [in] prop Characteristic properties.
 * @return A string representation of characteristic properties.
 */
String BLEUtils::characteristicPropertiesToString(uint8_t prop) {
  String res = "broadcast: ";
  res += ((prop & ESP_GATT_CHAR_PROP_BIT_BROADCAST) ? "1" : "0");
  res += ", read: ";
  res += ((prop & ESP_GATT_CHAR_PROP_BIT_READ) ? "1" : "0");
  res += ", write_nr: ";
  res += ((prop & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) ? "1" : "0");
  res += ", write: ";
  res += ((prop & ESP_GATT_CHAR_PROP_BIT_WRITE) ? "1" : "0");
  res += ", notify: ";
  res += ((prop & ESP_GATT_CHAR_PROP_BIT_NOTIFY) ? "1" : "0");
  res += ", indicate: ";
  res += ((prop & ESP_GATT_CHAR_PROP_BIT_INDICATE) ? "1" : "0");
  res += ", auth: ";
  res += ((prop & ESP_GATT_CHAR_PROP_BIT_AUTH) ? "1" : "0");
  return res;
}  // characteristicPropertiesToString

/**
 * @brief Build a printable string of memory range.
 * Create a string representation of a piece of memory. Only printable characters will be included
 * while those that are not printable will be replaced with '.'.
 * @param [in] source Start of memory.
 * @param [in] length Length of memory.
 * @return A string representation of a piece of memory.
 */
String BLEUtils::buildPrintData(uint8_t *source, size_t length) {
  String res;
  for (int i = 0; i < length; i++) {
    char c = *source;
    res += (isprint(c) ? c : '.');
    source++;
  }
  return res;
}  // buildPrintData

/**
 * @brief Convert an esp_gatt_id_t to a string.
 */
static String gattIdToString(esp_gatt_id_t gattId) {
  String res = "uuid: " + BLEUUID(gattId.uuid).toString() + ", inst_id: ";
  char val[8];
  snprintf(val, sizeof(val), "%u", gattId.inst_id);
  res += val;
  return res;
}  // gattIdToString

/**
 * @brief Convert an esp_ble_addr_type_t to a string representation.
 */
const char *BLEUtils::addressTypeToString(esp_ble_addr_type_t type) {
  switch (type) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    case BLE_ADDR_TYPE_PUBLIC:     return "BLE_ADDR_TYPE_PUBLIC";
    case BLE_ADDR_TYPE_RANDOM:     return "BLE_ADDR_TYPE_RANDOM";
    case BLE_ADDR_TYPE_RPA_PUBLIC: return "BLE_ADDR_TYPE_RPA_PUBLIC";
    case BLE_ADDR_TYPE_RPA_RANDOM: return "BLE_ADDR_TYPE_RPA_RANDOM";
#endif
    default: return " esp_ble_addr_type_t";
  }
}  // addressTypeToString

/**
 * @brief Convert the BLE Advertising Data flags to a string.
 * @param adFlags The flags to convert
 * @return String A string representation of the advertising flags.
 */
String BLEUtils::adFlagsToString(uint8_t adFlags) {
  String res;
  if (adFlags & (1 << 0)) {
    res += "[LE Limited Discoverable Mode] ";
  }
  if (adFlags & (1 << 1)) {
    res += "[LE General Discoverable Mode] ";
  }
  if (adFlags & (1 << 2)) {
    res += "[BR/EDR Not Supported] ";
  }
  if (adFlags & (1 << 3)) {
    res += "[Simultaneous LE and BR/EDR to Same Device Capable (Controller)] ";
  }
  if (adFlags & (1 << 4)) {
    res += "[Simultaneous LE and BR/EDR to Same Device Capable (Host)] ";
  }
  return res;
}  // adFlagsToString

esp_gatt_id_t BLEUtils::buildGattId(esp_bt_uuid_t uuid, uint8_t inst_id) {
  esp_gatt_id_t retGattId;
  retGattId.uuid = uuid;
  retGattId.inst_id = inst_id;
  return retGattId;
}

esp_gatt_srvc_id_t BLEUtils::buildGattSrvcId(esp_gatt_id_t gattId, bool is_primary) {
  esp_gatt_srvc_id_t retSrvcId;
  retSrvcId.id = gattId;
  retSrvcId.is_primary = is_primary;
  return retSrvcId;
}

/**
 * @brief Convert a close/disconnect reason to a string.
 * @param [in] reason The close reason.
 * @return A string representation of the reason.
 */
String BLEUtils::gattCloseReasonToString(esp_gatt_conn_reason_t reason) {
  switch (reason) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    case ESP_GATT_CONN_UNKNOWN:
    {
      return "ESP_GATT_CONN_UNKNOWN";
    }
    case ESP_GATT_CONN_L2C_FAILURE:
    {
      return "ESP_GATT_CONN_L2C_FAILURE";
    }
    case ESP_GATT_CONN_TIMEOUT:
    {
      return "ESP_GATT_CONN_TIMEOUT";
    }
    case ESP_GATT_CONN_TERMINATE_PEER_USER:
    {
      return "ESP_GATT_CONN_TERMINATE_PEER_USER";
    }
    case ESP_GATT_CONN_TERMINATE_LOCAL_HOST:
    {
      return "ESP_GATT_CONN_TERMINATE_LOCAL_HOST";
    }
    case ESP_GATT_CONN_FAIL_ESTABLISH:
    {
      return "ESP_GATT_CONN_FAIL_ESTABLISH";
    }
    case ESP_GATT_CONN_LMP_TIMEOUT:
    {
      return "ESP_GATT_CONN_LMP_TIMEOUT";
    }
    case ESP_GATT_CONN_CONN_CANCEL:
    {
      return "ESP_GATT_CONN_CONN_CANCEL";
    }
    case ESP_GATT_CONN_NONE:
    {
      return "ESP_GATT_CONN_NONE";
    }
#endif
    default:
    {
      return "Unknown";
    }
  }
}  // gattCloseReasonToString

String BLEUtils::gattClientEventTypeToString(esp_gattc_cb_event_t eventType) {
  switch (eventType) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    case ESP_GATTC_ACL_EVT:            return "ESP_GATTC_ACL_EVT";
    case ESP_GATTC_ADV_DATA_EVT:       return "ESP_GATTC_ADV_DATA_EVT";
    case ESP_GATTC_ADV_VSC_EVT:        return "ESP_GATTC_ADV_VSC_EVT";
    case ESP_GATTC_BTH_SCAN_CFG_EVT:   return "ESP_GATTC_BTH_SCAN_CFG_EVT";
    case ESP_GATTC_BTH_SCAN_DIS_EVT:   return "ESP_GATTC_BTH_SCAN_DIS_EVT";
    case ESP_GATTC_BTH_SCAN_ENB_EVT:   return "ESP_GATTC_BTH_SCAN_ENB_EVT";
    case ESP_GATTC_BTH_SCAN_PARAM_EVT: return "ESP_GATTC_BTH_SCAN_PARAM_EVT";
    case ESP_GATTC_BTH_SCAN_RD_EVT:    return "ESP_GATTC_BTH_SCAN_RD_EVT";
    case ESP_GATTC_BTH_SCAN_THR_EVT:   return "ESP_GATTC_BTH_SCAN_THR_EVT";
    case ESP_GATTC_CANCEL_OPEN_EVT:    return "ESP_GATTC_CANCEL_OPEN_EVT";
    case ESP_GATTC_CFG_MTU_EVT:        return "ESP_GATTC_CFG_MTU_EVT";
    case ESP_GATTC_CLOSE_EVT:          return "ESP_GATTC_CLOSE_EVT";
    case ESP_GATTC_CONGEST_EVT:        return "ESP_GATTC_CONGEST_EVT";
    case ESP_GATTC_CONNECT_EVT:        return "ESP_GATTC_CONNECT_EVT";
    case ESP_GATTC_DISCONNECT_EVT:     return "ESP_GATTC_DISCONNECT_EVT";
    case ESP_GATTC_ENC_CMPL_CB_EVT:    return "ESP_GATTC_ENC_CMPL_CB_EVT";
    case ESP_GATTC_EXEC_EVT:
      return "ESP_GATTC_EXEC_EVT";
      //case ESP_GATTC_GET_CHAR_EVT:
      //			return "ESP_GATTC_GET_CHAR_EVT";
      //case ESP_GATTC_GET_DESCR_EVT:
      //			return "ESP_GATTC_GET_DESCR_EVT";
      //case ESP_GATTC_GET_INCL_SRVC_EVT:
      //			return "ESP_GATTC_GET_INCL_SRVC_EVT";
    case ESP_GATTC_MULT_ADV_DATA_EVT:    return "ESP_GATTC_MULT_ADV_DATA_EVT";
    case ESP_GATTC_MULT_ADV_DIS_EVT:     return "ESP_GATTC_MULT_ADV_DIS_EVT";
    case ESP_GATTC_MULT_ADV_ENB_EVT:     return "ESP_GATTC_MULT_ADV_ENB_EVT";
    case ESP_GATTC_MULT_ADV_UPD_EVT:     return "ESP_GATTC_MULT_ADV_UPD_EVT";
    case ESP_GATTC_NOTIFY_EVT:           return "ESP_GATTC_NOTIFY_EVT";
    case ESP_GATTC_OPEN_EVT:             return "ESP_GATTC_OPEN_EVT";
    case ESP_GATTC_PREP_WRITE_EVT:       return "ESP_GATTC_PREP_WRITE_EVT";
    case ESP_GATTC_READ_CHAR_EVT:        return "ESP_GATTC_READ_CHAR_EVT";
    case ESP_GATTC_REG_EVT:              return "ESP_GATTC_REG_EVT";
    case ESP_GATTC_REG_FOR_NOTIFY_EVT:   return "ESP_GATTC_REG_FOR_NOTIFY_EVT";
    case ESP_GATTC_SCAN_FLT_CFG_EVT:     return "ESP_GATTC_SCAN_FLT_CFG_EVT";
    case ESP_GATTC_SCAN_FLT_PARAM_EVT:   return "ESP_GATTC_SCAN_FLT_PARAM_EVT";
    case ESP_GATTC_SCAN_FLT_STATUS_EVT:  return "ESP_GATTC_SCAN_FLT_STATUS_EVT";
    case ESP_GATTC_SEARCH_CMPL_EVT:      return "ESP_GATTC_SEARCH_CMPL_EVT";
    case ESP_GATTC_SEARCH_RES_EVT:       return "ESP_GATTC_SEARCH_RES_EVT";
    case ESP_GATTC_SRVC_CHG_EVT:         return "ESP_GATTC_SRVC_CHG_EVT";
    case ESP_GATTC_READ_DESCR_EVT:       return "ESP_GATTC_READ_DESCR_EVT";
    case ESP_GATTC_UNREG_EVT:            return "ESP_GATTC_UNREG_EVT";
    case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: return "ESP_GATTC_UNREG_FOR_NOTIFY_EVT";
    case ESP_GATTC_WRITE_CHAR_EVT:       return "ESP_GATTC_WRITE_CHAR_EVT";
    case ESP_GATTC_WRITE_DESCR_EVT:      return "ESP_GATTC_WRITE_DESCR_EVT";
#endif
    default: log_v("Unknown GATT Client event type: %d", eventType); return "Unknown";
  }
}  // gattClientEventTypeToString

/**
 * @brief Return a string representation of a GATT server event code.
 * @param [in] eventType A GATT server event code.
 * @return A string representation of the GATT server event code.
 */
String BLEUtils::gattServerEventTypeToString(esp_gatts_cb_event_t eventType) {
  switch (eventType) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    case ESP_GATTS_REG_EVT:                 return "ESP_GATTS_REG_EVT";
    case ESP_GATTS_READ_EVT:                return "ESP_GATTS_READ_EVT";
    case ESP_GATTS_WRITE_EVT:               return "ESP_GATTS_WRITE_EVT";
    case ESP_GATTS_EXEC_WRITE_EVT:          return "ESP_GATTS_EXEC_WRITE_EVT";
    case ESP_GATTS_MTU_EVT:                 return "ESP_GATTS_MTU_EVT";
    case ESP_GATTS_CONF_EVT:                return "ESP_GATTS_CONF_EVT";
    case ESP_GATTS_UNREG_EVT:               return "ESP_GATTS_UNREG_EVT";
    case ESP_GATTS_CREATE_EVT:              return "ESP_GATTS_CREATE_EVT";
    case ESP_GATTS_ADD_INCL_SRVC_EVT:       return "ESP_GATTS_ADD_INCL_SRVC_EVT";
    case ESP_GATTS_ADD_CHAR_EVT:            return "ESP_GATTS_ADD_CHAR_EVT";
    case ESP_GATTS_ADD_CHAR_DESCR_EVT:      return "ESP_GATTS_ADD_CHAR_DESCR_EVT";
    case ESP_GATTS_DELETE_EVT:              return "ESP_GATTS_DELETE_EVT";
    case ESP_GATTS_START_EVT:               return "ESP_GATTS_START_EVT";
    case ESP_GATTS_STOP_EVT:                return "ESP_GATTS_STOP_EVT";
    case ESP_GATTS_CONNECT_EVT:             return "ESP_GATTS_CONNECT_EVT";
    case ESP_GATTS_DISCONNECT_EVT:          return "ESP_GATTS_DISCONNECT_EVT";
    case ESP_GATTS_OPEN_EVT:                return "ESP_GATTS_OPEN_EVT";
    case ESP_GATTS_CANCEL_OPEN_EVT:         return "ESP_GATTS_CANCEL_OPEN_EVT";
    case ESP_GATTS_CLOSE_EVT:               return "ESP_GATTS_CLOSE_EVT";
    case ESP_GATTS_LISTEN_EVT:              return "ESP_GATTS_LISTEN_EVT";
    case ESP_GATTS_CONGEST_EVT:             return "ESP_GATTS_CONGEST_EVT";
    case ESP_GATTS_RESPONSE_EVT:            return "ESP_GATTS_RESPONSE_EVT";
    case ESP_GATTS_CREAT_ATTR_TAB_EVT:      return "ESP_GATTS_CREAT_ATTR_TAB_EVT";
    case ESP_GATTS_SET_ATTR_VAL_EVT:        return "ESP_GATTS_SET_ATTR_VAL_EVT";
    case ESP_GATTS_SEND_SERVICE_CHANGE_EVT: return "ESP_GATTS_SEND_SERVICE_CHANGE_EVT";
#endif
    default: return "Unknown";
  }
}  // gattServerEventTypeToString

/**
 * @brief Convert a BLE device type to a string.
 * @param [in] type The device type.
 */
const char *BLEUtils::devTypeToString(esp_bt_dev_type_t type) {
  switch (type) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    case ESP_BT_DEVICE_TYPE_BREDR: return "ESP_BT_DEVICE_TYPE_BREDR";
    case ESP_BT_DEVICE_TYPE_BLE:   return "ESP_BT_DEVICE_TYPE_BLE";
    case ESP_BT_DEVICE_TYPE_DUMO:  return "ESP_BT_DEVICE_TYPE_DUMO";
#endif
    default: return "Unknown";
  }
}  // devTypeToString

/**
 * @brief Dump the GAP event to the log.
 */
void BLEUtils::dumpGapEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
  log_v("Received a GAP event: %s", gapEventToString(event));
  switch (event) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    // ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT
    // adv_data_cmpl
    // - esp_bt_status_t
    case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
    {
      log_v("[status: %d]", param->adv_data_cmpl.status);
      break;
    }  // ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT

    // ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT
    //
    // adv_data_raw_cmpl
    // - esp_bt_status_t status
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
    {
      log_v("[status: %d]", param->adv_data_raw_cmpl.status);
      break;
    }  // ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT

    // ESP_GAP_BLE_ADV_START_COMPLETE_EVT
    //
    // adv_start_cmpl
    // - esp_bt_status_t status
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
    {
      log_v("[status: %d]", param->adv_start_cmpl.status);
      break;
    }  // ESP_GAP_BLE_ADV_START_COMPLETE_EVT

    // ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT
    //
    // adv_stop_cmpl
    // - esp_bt_status_t status
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
    {
      log_v("[status: %d]", param->adv_stop_cmpl.status);
      break;
    }  // ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT

    // ESP_GAP_BLE_AUTH_CMPL_EVT
    //
    // auth_cmpl
    // - esp_bd_addr_t bd_addr
    // - bool key_present
    // - esp_link_key key
    // - bool success
    // - uint8_t fail_reason
    // - esp_bd_addr_type_t addr_type
    // - esp_bt_dev_type_t dev_type
    case ESP_GAP_BLE_AUTH_CMPL_EVT:
    {
      log_v(
        "[bd_addr: %s, key_present: %d, key: ***, key_type: %u, success: %d, fail_reason: %d, addr_type: ***, dev_type: %s]",
        BLEAddress(param->ble_security.auth_cmpl.bd_addr).toString().c_str(), param->ble_security.auth_cmpl.key_present, param->ble_security.auth_cmpl.key_type,
        param->ble_security.auth_cmpl.success, param->ble_security.auth_cmpl.fail_reason, BLEUtils::devTypeToString(param->ble_security.auth_cmpl.dev_type)
      );
      break;
    }  // ESP_GAP_BLE_AUTH_CMPL_EVT

    // ESP_GAP_BLE_CLEAR_BOND_DEV_COMPLETE_EVT
    //
    // clear_bond_dev_cmpl
    // - esp_bt_status_t status
    case ESP_GAP_BLE_CLEAR_BOND_DEV_COMPLETE_EVT:
    {
      log_v("[status: %d]", param->clear_bond_dev_cmpl.status);
      break;
    }  // ESP_GAP_BLE_CLEAR_BOND_DEV_COMPLETE_EVT

    // ESP_GAP_BLE_LOCAL_IR_EVT
    case ESP_GAP_BLE_LOCAL_IR_EVT:
    {
      break;
    }  // ESP_GAP_BLE_LOCAL_IR_EVT

    // ESP_GAP_BLE_LOCAL_ER_EVT
    case ESP_GAP_BLE_LOCAL_ER_EVT:
    {
      break;
    }  // ESP_GAP_BLE_LOCAL_ER_EVT

    // ESP_GAP_BLE_NC_REQ_EVT
    case ESP_GAP_BLE_NC_REQ_EVT:
    {
      log_v(
        "[bd_addr: %s, passkey: %06" PRIu32 "]", BLEAddress(param->ble_security.key_notif.bd_addr).toString().c_str(), param->ble_security.key_notif.passkey
      );
      break;
    }  // ESP_GAP_BLE_NC_REQ_EVT

    // ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT
    //
    // read_rssi_cmpl
    // - esp_bt_status_t status
    // - int8_t rssi
    // - esp_bd_addr_t remote_addr
    case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
    {
      log_v(
        "[status: %d, rssi: %d, remote_addr: %s]", param->read_rssi_cmpl.status, param->read_rssi_cmpl.rssi,
        BLEAddress(param->read_rssi_cmpl.remote_addr).toString().c_str()
      );
      break;
    }  // ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT

    // ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT
    //
    // scan_param_cmpl.
    // - esp_bt_status_t status
    case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
    {
      log_v("[status: %d]", param->scan_param_cmpl.status);
      break;
    }  // ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT

    // ESP_GAP_BLE_SCAN_RESULT_EVT
    //
    // scan_rst:
    // - search_evt
    // - bda
    // - dev_type
    // - ble_addr_type
    // - ble_evt_type
    // - rssi
    // - ble_adv
    // - flag
    // - num_resps
    // - adv_data_len
    // - scan_rsp_len
    case ESP_GAP_BLE_SCAN_RESULT_EVT:
    {
      switch (param->scan_rst.search_evt) {
        case ESP_GAP_SEARCH_INQ_RES_EVT:
        {
          log_v(
            "search_evt: %s, bda: %s, dev_type: %s, ble_addr_type: %s, ble_evt_type: %s, rssi: %d, ble_adv: ??, flag: %d (%s), num_resps: %d, adv_data_len: "
            "%d, scan_rsp_len: %d",
            searchEventTypeToString(param->scan_rst.search_evt), BLEAddress(param->scan_rst.bda).toString().c_str(), devTypeToString(param->scan_rst.dev_type),
            addressTypeToString(param->scan_rst.ble_addr_type), eventTypeToString(param->scan_rst.ble_evt_type), param->scan_rst.rssi, param->scan_rst.flag,
            adFlagsToString(param->scan_rst.flag).c_str(), param->scan_rst.num_resps, param->scan_rst.adv_data_len, param->scan_rst.scan_rsp_len
          );
          break;
        }  // ESP_GAP_SEARCH_INQ_RES_EVT

        default:
        {
          log_v("search_evt: %s", searchEventTypeToString(param->scan_rst.search_evt));
          break;
        }
      }
      break;
    }  // ESP_GAP_BLE_SCAN_RESULT_EVT

    // ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT
    //
    // scan_rsp_data_cmpl
    // - esp_bt_status_t status
    case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
    {
      log_v("[status: %d]", param->scan_rsp_data_cmpl.status);
      break;
    }  // ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT

    // ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT
    case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT:
    {
      log_v("[status: %d]", param->scan_rsp_data_raw_cmpl.status);
      break;
    }  // ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT

    // ESP_GAP_BLE_SCAN_START_COMPLETE_EVT
    //
    // scan_start_cmpl
    // - esp_bt_status_t status
    case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
    {
      log_v("[status: %d]", param->scan_start_cmpl.status);
      break;
    }  // ESP_GAP_BLE_SCAN_START_COMPLETE_EVT

    // ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT
    //
    // scan_stop_cmpl
    // - esp_bt_status_t status
    case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
    {
      log_v("[status: %d]", param->scan_stop_cmpl.status);
      break;
    }  // ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT

    // ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT
    //
    // update_conn_params
    // - esp_bt_status_t status
    // - esp_bd_addr_t bda
    // - uint16_t min_int
    // - uint16_t max_int
    // - uint16_t latency
    // - uint16_t conn_int
    // - uint16_t timeout
    case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
    {
      log_v(
        "[status: %d, bd_addr: %s, min_int: %u, max_int: %u, latency: %u, conn_int: %u, timeout: %u]", param->update_conn_params.status,
        BLEAddress(param->update_conn_params.bda).toString().c_str(), param->update_conn_params.min_int, param->update_conn_params.max_int,
        param->update_conn_params.latency, param->update_conn_params.conn_int, param->update_conn_params.timeout
      );
      break;
    }  // ESP_GAP_BLE_SCAN_UPDATE_CONN_PARAMS_EVT

    // ESP_GAP_BLE_SEC_REQ_EVT
    case ESP_GAP_BLE_SEC_REQ_EVT:
    {
      log_v("[bd_addr: %s]", BLEAddress(param->ble_security.ble_req.bd_addr).toString().c_str());
      break;
    }  // ESP_GAP_BLE_SEC_REQ_EVT
#endif
    default:
    {
      log_v("*** dumpGapEvent: Logger not coded ***");
      break;
    }  // default
  }  // switch
}  // dumpGapEvent

/**
 * @brief Decode and dump a GATT client event
 *
 * @param [in] event The type of event received.
 * @param [in] evtParam The data associated with the event.
 */
void BLEUtils::dumpGattClientEvent(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *evtParam) {

  //esp_ble_gattc_cb_param_t* evtParam = (esp_ble_gattc_cb_param_t*) param;
  log_v("GATT Event: %s", BLEUtils::gattClientEventTypeToString(event).c_str());
  switch (event) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    // ESP_GATTC_CLOSE_EVT
    //
    // close:
    // - esp_gatt_status_t      status
    // - uint16_t               conn_id
    // - esp_bd_addr_t          remote_bda
    // - esp_gatt_conn_reason_t reason
    case ESP_GATTC_CLOSE_EVT:
    {
      log_v(
        "[status: %s, reason:%s, conn_id: %u]", BLEUtils::gattStatusToString(evtParam->close.status).c_str(),
        BLEUtils::gattCloseReasonToString(evtParam->close.reason).c_str(), evtParam->close.conn_id
      );
      break;
    }

    // ESP_GATTC_CONNECT_EVT
    //
    // connect:
    // - esp_gatt_status_t status
    // - uint16_t          conn_id
    // - esp_bd_addr_t     remote_bda
    case ESP_GATTC_CONNECT_EVT:
    {
      log_v("[conn_id: %u, remote_bda: %s]", evtParam->connect.conn_id, BLEAddress(evtParam->connect.remote_bda).toString().c_str());
      break;
    }

    // ESP_GATTC_DISCONNECT_EVT
    //
    // disconnect:
    // - esp_gatt_conn_reason_t reason
    // - uint16_t               conn_id
    // - esp_bd_addr_t          remote_bda
    case ESP_GATTC_DISCONNECT_EVT:
    {
      log_v(
        "[reason: %s, conn_id: %u, remote_bda: %s]", BLEUtils::gattCloseReasonToString(evtParam->disconnect.reason).c_str(), evtParam->disconnect.conn_id,
        BLEAddress(evtParam->disconnect.remote_bda).toString().c_str()
      );
      break;
    }  // ESP_GATTC_DISCONNECT_EVT

    // ESP_GATTC_GET_CHAR_EVT
    //
    // get_char:
    // - esp_gatt_status_t    status
    // - uin1t6_t             conn_id
    // - esp_gatt_srvc_id_t   srvc_id
    // - esp_gatt_id_t        char_id
    // - esp_gatt_char_prop_t char_prop
    /*
		case ESP_GATTC_GET_CHAR_EVT: {

			// If the status of the event shows that we have a value other than ESP_GATT_OK then the
			// characteristic fields are not set to a usable value .. so don't try and log them.
			if (evtParam->get_char.status == ESP_GATT_OK) {
				String description = "Unknown";
				if (evtParam->get_char.char_id.uuid.len == ESP_UUID_LEN_16) {
					description = BLEUtils::gattCharacteristicUUIDToString(evtParam->get_char.char_id.uuid.uuid.uuid16);
				}
				log_v("[status: %s, conn_id: %u, srvc_id: %s, char_id: %s [description: %s]\nchar_prop: %s]",
					BLEUtils::gattStatusToString(evtParam->get_char.status).c_str(),
					evtParam->get_char.conn_id,
					BLEUtils::gattServiceIdToString(evtParam->get_char.srvc_id).c_str(),
					gattIdToString(evtParam->get_char.char_id).c_str(),
					description.c_str(),
					BLEUtils::characteristicPropertiesToString(evtParam->get_char.char_prop).c_str()
				);
			} else {
				log_v("[status: %s, conn_id: %u, srvc_id: %s]",
					BLEUtils::gattStatusToString(evtParam->get_char.status).c_str(),
					evtParam->get_char.conn_id,
					BLEUtils::gattServiceIdToString(evtParam->get_char.srvc_id).c_str()
				);
			}
			break;
		} // ESP_GATTC_GET_CHAR_EVT
		*/

    // ESP_GATTC_NOTIFY_EVT
    //
    // notify
    // uint16_t           conn_id
    // esp_bd_addr_t      remote_bda
    // handle             handle
    // uint16_t           value_len
    // uint8_t*           value
    // bool               is_notify
    //
    case ESP_GATTC_NOTIFY_EVT:
    {
      log_v(
        "[conn_id: %u, remote_bda: %s, handle: %u 0x%02x, value_len: %u, is_notify: %d]", evtParam->notify.conn_id,
        BLEAddress(evtParam->notify.remote_bda).toString().c_str(), evtParam->notify.handle, evtParam->notify.handle, evtParam->notify.value_len,
        evtParam->notify.is_notify
      );
      break;
    }

    // ESP_GATTC_OPEN_EVT
    //
    // open:
    // - esp_gatt_status_t status
    // - uint16_t          conn_id
    // - esp_bd_addr_t     remote_bda
    // - uint16_t          mtu
    //
    case ESP_GATTC_OPEN_EVT:
    {
      log_v(
        "[status: %s, conn_id: %u, remote_bda: %s, mtu: %u]", BLEUtils::gattStatusToString(evtParam->open.status).c_str(), evtParam->open.conn_id,
        BLEAddress(evtParam->open.remote_bda).toString().c_str(), evtParam->open.mtu
      );
      break;
    }  // ESP_GATTC_OPEN_EVT

    // ESP_GATTC_READ_CHAR_EVT
    //
    // Callback to indicate that requested data that we wanted to read is now available.
    //
    // read:
    // esp_gatt_status_t  status
    // uint16_t           conn_id
    // uint16_t           handle
    // uint8_t*           value
    // uint16_t           value_type
    // uint16_t           value_len
    case ESP_GATTC_READ_CHAR_EVT:
    {
      log_v(
        "[status: %s, conn_id: %u, handle: %u 0x%02x, value_len: %u]", BLEUtils::gattStatusToString(evtParam->read.status).c_str(), evtParam->read.conn_id,
        evtParam->read.handle, evtParam->read.handle, evtParam->read.value_len
      );
      if (evtParam->read.status == ESP_GATT_OK) {
        GeneralUtils::hexDump(evtParam->read.value, evtParam->read.value_len);
        /*
				char* pHexData = BLEUtils::buildHexData(nullptr, evtParam->read.value, evtParam->read.value_len);
				log_v("value: %s \"%s\"", pHexData, BLEUtils::buildPrintData(evtParam->read.value, evtParam->read.value_len).c_str());
				free(pHexData);
				*/
      }
      break;
    }  // ESP_GATTC_READ_CHAR_EVT

    // ESP_GATTC_REG_EVT
    //
    // reg:
    // - esp_gatt_status_t status
    // - uint16_t          app_id
    case ESP_GATTC_REG_EVT:
    {
      log_v("[status: %s, app_id: 0x%x]", BLEUtils::gattStatusToString(evtParam->reg.status).c_str(), evtParam->reg.app_id);
      break;
    }  // ESP_GATTC_REG_EVT

    // ESP_GATTC_REG_FOR_NOTIFY_EVT
    //
    // reg_for_notify:
    // - esp_gatt_status_t status
    // - uint16_t          handle
    case ESP_GATTC_REG_FOR_NOTIFY_EVT:
    {
      log_v(
        "[status: %s, handle: %u 0x%02x]", BLEUtils::gattStatusToString(evtParam->reg_for_notify.status).c_str(), evtParam->reg_for_notify.handle,
        evtParam->reg_for_notify.handle
      );
      break;
    }  // ESP_GATTC_REG_FOR_NOTIFY_EVT

    // ESP_GATTC_SEARCH_CMPL_EVT
    //
    // search_cmpl:
    // - esp_gatt_status_t status
    // - uint16_t          conn_id
    case ESP_GATTC_SEARCH_CMPL_EVT:
    {
      log_v("[status: %s, conn_id: %u]", BLEUtils::gattStatusToString(evtParam->search_cmpl.status).c_str(), evtParam->search_cmpl.conn_id);
      break;
    }  // ESP_GATTC_SEARCH_CMPL_EVT

    // ESP_GATTC_SEARCH_RES_EVT
    //
    // search_res:
    // - uint16_t      conn_id
    // - uint16_t      start_handle
    // - uint16_t      end_handle
    // - esp_gatt_id_t srvc_id
    case ESP_GATTC_SEARCH_RES_EVT:
    {
      log_v(
        "[conn_id: %u, start_handle: %u 0x%02x, end_handle: %u 0x%02x, srvc_id: %s", evtParam->search_res.conn_id, evtParam->search_res.start_handle,
        evtParam->search_res.start_handle, evtParam->search_res.end_handle, evtParam->search_res.end_handle,
        gattIdToString(evtParam->search_res.srvc_id).c_str()
      );
      break;
    }  // ESP_GATTC_SEARCH_RES_EVT

    // ESP_GATTC_WRITE_CHAR_EVT
    //
    // write:
    // - esp_gatt_status_t status
    // - uint16_t          conn_id
    // - uint16_t          handle
    // - uint16_t          offset
    case ESP_GATTC_WRITE_CHAR_EVT:
    {
      log_v(
        "[status: %s, conn_id: %u, handle: %u 0x%02x, offset: %u]", BLEUtils::gattStatusToString(evtParam->write.status).c_str(), evtParam->write.conn_id,
        evtParam->write.handle, evtParam->write.handle, evtParam->write.offset
      );
      break;
    }  // ESP_GATTC_WRITE_CHAR_EVT
#endif
    default: break;
  }
}  // dumpGattClientEvent

/**
 * @brief Dump the details of a GATT server event.
 * A GATT Server event is a callback received from the BLE subsystem when we are acting as a BLE
 * server.  The callback indicates the type of event in the `event` field.  The `evtParam` is a
 * union of structures where we can use the `event` to indicate which of the structures has been
 * populated and hence is valid.
 *
 * @param [in] event The event type that was posted.
 * @param [in] evtParam A union of structures only one of which is populated.
 */
void BLEUtils::dumpGattServerEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *evtParam) {
  log_v("GATT ServerEvent: %s", BLEUtils::gattServerEventTypeToString(event).c_str());
  switch (event) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG

    case ESP_GATTS_ADD_CHAR_DESCR_EVT:
    {
      log_v(
        "[status: %s, attr_handle: %u 0x%02x, service_handle: %u 0x%02x, char_uuid: %s]", gattStatusToString(evtParam->add_char_descr.status).c_str(),
        evtParam->add_char_descr.attr_handle, evtParam->add_char_descr.attr_handle, evtParam->add_char_descr.service_handle,
        evtParam->add_char_descr.service_handle, BLEUUID(evtParam->add_char_descr.descr_uuid).toString().c_str()
      );
      break;
    }  // ESP_GATTS_ADD_CHAR_DESCR_EVT

    case ESP_GATTS_ADD_CHAR_EVT:
    {
      if (evtParam->add_char.status == ESP_GATT_OK) {
        log_v(
          "[status: %s, attr_handle: %u 0x%02x, service_handle: %u 0x%02x, char_uuid: %s]", gattStatusToString(evtParam->add_char.status).c_str(),
          evtParam->add_char.attr_handle, evtParam->add_char.attr_handle, evtParam->add_char.service_handle, evtParam->add_char.service_handle,
          BLEUUID(evtParam->add_char.char_uuid).toString().c_str()
        );
      } else {
        log_e(
          "[status: %s, attr_handle: %u 0x%02x, service_handle: %u 0x%02x, char_uuid: %s]", gattStatusToString(evtParam->add_char.status).c_str(),
          evtParam->add_char.attr_handle, evtParam->add_char.attr_handle, evtParam->add_char.service_handle, evtParam->add_char.service_handle,
          BLEUUID(evtParam->add_char.char_uuid).toString().c_str()
        );
      }
      break;
    }  // ESP_GATTS_ADD_CHAR_EVT

    // ESP_GATTS_CONF_EVT
    //
    // conf:
    // - esp_gatt_status_t status  – The status code.
    // - uint16_t          conn_id – The connection used.
    case ESP_GATTS_CONF_EVT:
    {
      log_v("[status: %s, conn_id: 0x%02x]", gattStatusToString(evtParam->conf.status).c_str(), evtParam->conf.conn_id);
      break;
    }  // ESP_GATTS_CONF_EVT

    case ESP_GATTS_CONGEST_EVT:
    {
      log_v("[conn_id: %u, congested: %d]", evtParam->congest.conn_id, evtParam->congest.congested);
      break;
    }  // ESP_GATTS_CONGEST_EVT

    case ESP_GATTS_CONNECT_EVT:
    {
      log_v("[conn_id: %u, remote_bda: %s]", evtParam->connect.conn_id, BLEAddress(evtParam->connect.remote_bda).toString().c_str());
      break;
    }  // ESP_GATTS_CONNECT_EVT

    case ESP_GATTS_CREATE_EVT:
    {
      log_v(
        "[status: %s, service_handle: %u 0x%02x, service_id: [%s]]", gattStatusToString(evtParam->create.status).c_str(), evtParam->create.service_handle,
        evtParam->create.service_handle, gattServiceIdToString(evtParam->create.service_id).c_str()
      );
      break;
    }  // ESP_GATTS_CREATE_EVT

    case ESP_GATTS_DISCONNECT_EVT:
    {
      log_v("[conn_id: %u, remote_bda: %s]", evtParam->connect.conn_id, BLEAddress(evtParam->connect.remote_bda).toString().c_str());
      break;
    }  // ESP_GATTS_DISCONNECT_EVT

    // ESP_GATTS_EXEC_WRITE_EVT
    // exec_write:
    // - uint16_t conn_id
    // - uint32_t trans_id
    // - esp_bd_addr_t bda
    // - uint8_t exec_write_flag
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE
    case ESP_GATTS_EXEC_WRITE_EVT:
    {
      char *pWriteFlagText;
      switch (evtParam->exec_write.exec_write_flag) {
        case ESP_GATT_PREP_WRITE_EXEC:
        {
          pWriteFlagText = (char *)"WRITE";
          break;
        }

        case ESP_GATT_PREP_WRITE_CANCEL:
        {
          pWriteFlagText = (char *)"CANCEL";
          break;
        }

        default: pWriteFlagText = (char *)"<Unknown>"; break;
      }

      log_v(
        "[conn_id: %u, trans_id: %" PRIu32 ", bda: %s, exec_write_flag: 0x%02x=%s]", evtParam->exec_write.conn_id, evtParam->exec_write.trans_id,
        BLEAddress(evtParam->exec_write.bda).toString().c_str(), evtParam->exec_write.exec_write_flag, pWriteFlagText
      );
      break;
    }  // ESP_GATTS_DISCONNECT_EVT
#endif

    case ESP_GATTS_MTU_EVT:
    {
      log_v("[conn_id: %u, mtu: %u]", evtParam->mtu.conn_id, evtParam->mtu.mtu);
      break;
    }  // ESP_GATTS_MTU_EVT

    case ESP_GATTS_READ_EVT:
    {
      log_v(
        "[conn_id: %u, trans_id: %" PRIu32 ", bda: %s, handle: 0x%02x, is_long: %d, need_rsp:%d]", evtParam->read.conn_id, evtParam->read.trans_id,
        BLEAddress(evtParam->read.bda).toString().c_str(), evtParam->read.handle, evtParam->read.is_long, evtParam->read.need_rsp
      );
      break;
    }  // ESP_GATTS_READ_EVT

    case ESP_GATTS_RESPONSE_EVT:
    {
      log_v("[status: %s, handle: 0x%02x]", gattStatusToString(evtParam->rsp.status).c_str(), evtParam->rsp.handle);
      break;
    }  // ESP_GATTS_RESPONSE_EVT

    case ESP_GATTS_REG_EVT:
    {
      log_v("[status: %s, app_id: %u]", gattStatusToString(evtParam->reg.status).c_str(), evtParam->reg.app_id);
      break;
    }  // ESP_GATTS_REG_EVT

    // ESP_GATTS_START_EVT
    //
    // start:
    // - esp_gatt_status_t status
    // - uint16_t          service_handle
    case ESP_GATTS_START_EVT:
    {
      log_v("[status: %s, service_handle: 0x%02x]", gattStatusToString(evtParam->start.status).c_str(), evtParam->start.service_handle);
      break;
    }  // ESP_GATTS_START_EVT

    // ESP_GATTS_WRITE_EVT
    //
    // write:
    // - uint16_t      conn_id  – The connection id.
    // - uint16_t      trans_id – The transfer id.
    // - esp_bd_addr_t bda      – The address of the partner.
    // - uint16_t      handle   – The attribute handle.
    // - uint16_t      offset   – The offset of the currently received within the whole value.
    // - bool          need_rsp – Do we need a response?
    // - bool          is_prep  – Is this a write prepare?  If set, then this is to be considered part of the received value and not the whole value.  A subsequent ESP_GATTS_EXEC_WRITE will mark the total.
    // - uint16_t      len      – The length of the incoming value part.
    // - uint8_t*      value    – The data for this value part.
    case ESP_GATTS_WRITE_EVT:
    {
      log_v(
        "[conn_id: %u, trans_id: %" PRIu32 ", bda: %s, handle: 0x%02x, offset: %u, need_rsp: %d, is_prep: %d, len: %u]", evtParam->write.conn_id,
        evtParam->write.trans_id, BLEAddress(evtParam->write.bda).toString().c_str(), evtParam->write.handle, evtParam->write.offset, evtParam->write.need_rsp,
        evtParam->write.is_prep, evtParam->write.len
      );
      char *pHex = buildHexData(nullptr, evtParam->write.value, evtParam->write.len);
      log_v("[Data: %s]", pHex);
      free(pHex);
      break;
    }  // ESP_GATTS_WRITE_EVT
#endif
    default: log_v("dumpGattServerEvent: *** NOT CODED ***"); break;
  }
}  // dumpGattServerEvent

/**
 * @brief Convert a BLE event type to a string.
 * @param [in] eventType The event type.
 * @return The event type as a string.
 */
const char *BLEUtils::eventTypeToString(esp_ble_evt_type_t eventType) {
  switch (eventType) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    case ESP_BLE_EVT_CONN_ADV:     return "ESP_BLE_EVT_CONN_ADV";
    case ESP_BLE_EVT_CONN_DIR_ADV: return "ESP_BLE_EVT_CONN_DIR_ADV";
    case ESP_BLE_EVT_DISC_ADV:     return "ESP_BLE_EVT_DISC_ADV";
    case ESP_BLE_EVT_NON_CONN_ADV: return "ESP_BLE_EVT_NON_CONN_ADV";
    case ESP_BLE_EVT_SCAN_RSP:     return "ESP_BLE_EVT_SCAN_RSP";
#endif
    default: log_v("Unknown esp_ble_evt_type_t: %d (0x%02x)", eventType, eventType); return "*** Unknown ***";
  }
}  // eventTypeToString

/**
 * @brief Convert a BT GAP event type to a string representation.
 * @param [in] eventType The type of event.
 * @return A string representation of the event type.
 */
const char *BLEUtils::gapEventToString(uint32_t eventType) {
  switch (eventType) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:          return "ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT";
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:      return "ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT";
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:             return "ESP_GAP_BLE_ADV_START_COMPLETE_EVT";
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:              /* !< When stop adv complete, the event comes */ return "ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT";
    case ESP_GAP_BLE_AUTH_CMPL_EVT:                      /* Authentication complete indication. */ return "ESP_GAP_BLE_AUTH_CMPL_EVT";
    case ESP_GAP_BLE_CLEAR_BOND_DEV_COMPLETE_EVT:        return "ESP_GAP_BLE_CLEAR_BOND_DEV_COMPLETE_EVT";
    case ESP_GAP_BLE_GET_BOND_DEV_COMPLETE_EVT:          return "ESP_GAP_BLE_GET_BOND_DEV_COMPLETE_EVT";
    case ESP_GAP_BLE_KEY_EVT:                            /* BLE  key event for peer device keys */ return "ESP_GAP_BLE_KEY_EVT";
    case ESP_GAP_BLE_LOCAL_IR_EVT:                       /* BLE local IR event */ return "ESP_GAP_BLE_LOCAL_IR_EVT";
    case ESP_GAP_BLE_LOCAL_ER_EVT:                       /* BLE local ER event */ return "ESP_GAP_BLE_LOCAL_ER_EVT";
    case ESP_GAP_BLE_NC_REQ_EVT:                         /* Numeric Comparison request event */ return "ESP_GAP_BLE_NC_REQ_EVT";
    case ESP_GAP_BLE_OOB_REQ_EVT:                        /* OOB request event */ return "ESP_GAP_BLE_OOB_REQ_EVT";
    case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:                  /* passkey notification event */ return "ESP_GAP_BLE_PASSKEY_NOTIF_EVT";
    case ESP_GAP_BLE_PASSKEY_REQ_EVT:                    /* passkey request event */ return "ESP_GAP_BLE_PASSKEY_REQ_EVT";
    case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:             return "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT";
    case ESP_GAP_BLE_REMOVE_BOND_DEV_COMPLETE_EVT:       return "ESP_GAP_BLE_REMOVE_BOND_DEV_COMPLETE_EVT";
    case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:        return "ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT";
    case ESP_GAP_BLE_SCAN_RESULT_EVT:                    return "ESP_GAP_BLE_SCAN_RESULT_EVT";
    case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT: return "ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT";
    case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:     return "ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT";
    case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:            return "ESP_GAP_BLE_SCAN_START_COMPLETE_EVT";
    case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:             return "ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT";
    case ESP_GAP_BLE_SEC_REQ_EVT:                        /* BLE  security request */ return "ESP_GAP_BLE_SEC_REQ_EVT";
    case ESP_GAP_BLE_SET_LOCAL_PRIVACY_COMPLETE_EVT:     return "ESP_GAP_BLE_SET_LOCAL_PRIVACY_COMPLETE_EVT";
    case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:        return "ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT";
    case ESP_GAP_BLE_SET_STATIC_RAND_ADDR_EVT:           return "ESP_GAP_BLE_SET_STATIC_RAND_ADDR_EVT";
    case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:             return "ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT";
#endif
    default: log_v("gapEventToString: Unknown event type %d 0x02x", eventType, eventType); return "Unknown event type";
  }
}  // gapEventToString

String BLEUtils::gattCharacteristicUUIDToString(uint32_t characteristicUUID) {
  const characteristicMap_t *p = g_characteristicsMappings;
  while (strlen(p->name) > 0) {
    if (p->assignedNumber == characteristicUUID) {
      return String(p->name);
    }
    p++;
  }
  return "Unknown";
}  // gattCharacteristicUUIDToString

/**
 * @brief Given the UUID for a BLE defined descriptor, return its string representation.
 * @param [in] descriptorUUID UUID of the descriptor to be returned as a string.
 * @return The string representation of a descriptor UUID.
 */
String BLEUtils::gattDescriptorUUIDToString(uint32_t descriptorUUID) {
  gattdescriptor_t *p = (gattdescriptor_t *)g_descriptor_ids;
  while (strlen(p->name) > 0) {
    if (p->assignedNumber == descriptorUUID) {
      return String(p->name);
    }
    p++;
  }
  return "";
}  // gattDescriptorUUIDToString

/**
 * @brief Return a string representation of an esp_gattc_service_elem_t.
 * @return A string representation of an esp_gattc_service_elem_t.
 */
String BLEUtils::gattcServiceElementToString(esp_gattc_service_elem_t *pGATTCServiceElement) {
  String res;
  char val[6];
  res += "[uuid: " + BLEUUID(pGATTCServiceElement->uuid).toString() + ", start_handle: ";
  snprintf(val, sizeof(val), "%u", pGATTCServiceElement->start_handle);
  res += val;
  res += " 0x";
  snprintf(val, sizeof(val), "%04x", pGATTCServiceElement->start_handle);
  res += val;
  res += ", end_handle: ";
  snprintf(val, sizeof(val), "%u", pGATTCServiceElement->end_handle);
  res += val;
  res += " 0x";
  snprintf(val, sizeof(val), "%04x", pGATTCServiceElement->end_handle);
  res += val;
  res += "]";
  return res;
}  // gattcServiceElementToString

/**
 * @brief Convert an esp_gatt_srvc_id_t to a string.
 */
String BLEUtils::gattServiceIdToString(esp_gatt_srvc_id_t srvcId) {
  return gattIdToString(srvcId.id);
}  // gattServiceIdToString

String BLEUtils::gattServiceToString(uint32_t serviceId) {
  gattService_t *p = (gattService_t *)g_gattServices;
  while (strlen(p->name) > 0) {
    if (p->assignedNumber == serviceId) {
      return String(p->name);
    }
    p++;
  }
  return "Unknown";
}  // gattServiceToString

/**
 * @brief Convert a GATT status to a string.
 *
 * @param [in] status The status to convert.
 * @return A string representation of the status.
 */
String BLEUtils::gattStatusToString(esp_gatt_status_t status) {
  switch (status) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    case ESP_GATT_OK:                   return "ESP_GATT_OK";
    case ESP_GATT_INVALID_HANDLE:       return "ESP_GATT_INVALID_HANDLE";
    case ESP_GATT_READ_NOT_PERMIT:      return "ESP_GATT_READ_NOT_PERMIT";
    case ESP_GATT_WRITE_NOT_PERMIT:     return "ESP_GATT_WRITE_NOT_PERMIT";
    case ESP_GATT_INVALID_PDU:          return "ESP_GATT_INVALID_PDU";
    case ESP_GATT_INSUF_AUTHENTICATION: return "ESP_GATT_INSUF_AUTHENTICATION";
    case ESP_GATT_REQ_NOT_SUPPORTED:    return "ESP_GATT_REQ_NOT_SUPPORTED";
    case ESP_GATT_INVALID_OFFSET:       return "ESP_GATT_INVALID_OFFSET";
    case ESP_GATT_INSUF_AUTHORIZATION:  return "ESP_GATT_INSUF_AUTHORIZATION";
    case ESP_GATT_PREPARE_Q_FULL:       return "ESP_GATT_PREPARE_Q_FULL";
    case ESP_GATT_NOT_FOUND:            return "ESP_GATT_NOT_FOUND";
    case ESP_GATT_NOT_LONG:             return "ESP_GATT_NOT_LONG";
    case ESP_GATT_INSUF_KEY_SIZE:       return "ESP_GATT_INSUF_KEY_SIZE";
    case ESP_GATT_INVALID_ATTR_LEN:     return "ESP_GATT_INVALID_ATTR_LEN";
    case ESP_GATT_ERR_UNLIKELY:         return "ESP_GATT_ERR_UNLIKELY";
    case ESP_GATT_INSUF_ENCRYPTION:     return "ESP_GATT_INSUF_ENCRYPTION";
    case ESP_GATT_UNSUPPORT_GRP_TYPE:   return "ESP_GATT_UNSUPPORT_GRP_TYPE";
    case ESP_GATT_INSUF_RESOURCE:       return "ESP_GATT_INSUF_RESOURCE";
    case ESP_GATT_NO_RESOURCES:         return "ESP_GATT_NO_RESOURCES";
    case ESP_GATT_INTERNAL_ERROR:       return "ESP_GATT_INTERNAL_ERROR";
    case ESP_GATT_WRONG_STATE:          return "ESP_GATT_WRONG_STATE";
    case ESP_GATT_DB_FULL:              return "ESP_GATT_DB_FULL";
    case ESP_GATT_BUSY:                 return "ESP_GATT_BUSY";
    case ESP_GATT_ERROR:                return "ESP_GATT_ERROR";
    case ESP_GATT_CMD_STARTED:          return "ESP_GATT_CMD_STARTED";
    case ESP_GATT_ILLEGAL_PARAMETER:    return "ESP_GATT_ILLEGAL_PARAMETER";
    case ESP_GATT_PENDING:              return "ESP_GATT_PENDING";
    case ESP_GATT_AUTH_FAIL:            return "ESP_GATT_AUTH_FAIL";
    case ESP_GATT_MORE:                 return "ESP_GATT_MORE";
    case ESP_GATT_INVALID_CFG:          return "ESP_GATT_INVALID_CFG";
    case ESP_GATT_SERVICE_STARTED:      return "ESP_GATT_SERVICE_STARTED";
    case ESP_GATT_ENCRYPTED_NO_MITM:    return "ESP_GATT_ENCRYPTED_NO_MITM";
    case ESP_GATT_NOT_ENCRYPTED:        return "ESP_GATT_NOT_ENCRYPTED";
    case ESP_GATT_CONGESTED:            return "ESP_GATT_CONGESTED";
    case ESP_GATT_DUP_REG:              return "ESP_GATT_DUP_REG";
    case ESP_GATT_ALREADY_OPEN:         return "ESP_GATT_ALREADY_OPEN";
    case ESP_GATT_CANCEL:               return "ESP_GATT_CANCEL";
    case ESP_GATT_STACK_RSP:            return "ESP_GATT_STACK_RSP";
    case ESP_GATT_APP_RSP:              return "ESP_GATT_APP_RSP";
    case ESP_GATT_UNKNOWN_ERROR:        return "ESP_GATT_UNKNOWN_ERROR";
    case ESP_GATT_CCC_CFG_ERR:          return "ESP_GATT_CCC_CFG_ERR";
    case ESP_GATT_PRC_IN_PROGRESS:      return "ESP_GATT_PRC_IN_PROGRESS";
    case ESP_GATT_OUT_OF_RANGE:         return "ESP_GATT_OUT_OF_RANGE";
#endif
    default: return "Unknown";
  }
}  // gattStatusToString

String BLEUtils::getMember(uint32_t memberId) {
  member_t *p = (member_t *)members_ids;

  while (strlen(p->name) > 0) {
    if (p->assignedNumber == memberId) {
      return String(p->name);
    }
    p++;
  }
  return "Unknown";
}

/**
 * @brief convert a GAP search event to a string.
 * @param [in] searchEvt
 * @return The search event type as a string.
 */
const char *BLEUtils::searchEventTypeToString(esp_gap_search_evt_t searchEvt) {
  switch (searchEvt) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    case ESP_GAP_SEARCH_INQ_RES_EVT:            return "ESP_GAP_SEARCH_INQ_RES_EVT";
    case ESP_GAP_SEARCH_INQ_CMPL_EVT:           return "ESP_GAP_SEARCH_INQ_CMPL_EVT";
    case ESP_GAP_SEARCH_DISC_RES_EVT:           return "ESP_GAP_SEARCH_DISC_RES_EVT";
    case ESP_GAP_SEARCH_DISC_BLE_RES_EVT:       return "ESP_GAP_SEARCH_DISC_BLE_RES_EVT";
    case ESP_GAP_SEARCH_DISC_CMPL_EVT:          return "ESP_GAP_SEARCH_DISC_CMPL_EVT";
    case ESP_GAP_SEARCH_DI_DISC_CMPL_EVT:       return "ESP_GAP_SEARCH_DI_DISC_CMPL_EVT";
    case ESP_GAP_SEARCH_SEARCH_CANCEL_CMPL_EVT: return "ESP_GAP_SEARCH_SEARCH_CANCEL_CMPL_EVT";
#endif
    default: log_v("Unknown event type: 0x%x", searchEvt); return "Unknown event type";
  }
}  // searchEventTypeToString

#endif  // CONFIG_BLUEDROID_ENABLED

/*****************************************************************************
 *                             NimBLE functions                              *
 *****************************************************************************/

#if defined(CONFIG_NIMBLE_ENABLED)

/**
 * @brief Construct a BLETaskData instance.
 * @param [in] pInstance An instance of the class that will be waiting.
 * @param [in] flags General purpose flags for the caller.
 * @param [in] buf A buffer for data.
 */
BLETaskData::BLETaskData(void *pInstance, int flags, void *buf) : m_pInstance{pInstance}, m_flags{flags}, m_pBuf{buf}, m_pHandle{xTaskGetCurrentTaskHandle()} {}

/**
 * @brief Destructor.
 */
BLETaskData::~BLETaskData() {}

/**
 * @brief A function for checking validity of connection parameters.
 * @param [in] params A pointer to the structure containing the parameters to check.
 * @return valid == 0 or error code.
 */
int BLEUtils::checkConnParams(ble_gap_conn_params *params) {
  /* Check connection interval min */
  if ((params->itvl_min < BLE_HCI_CONN_ITVL_MIN) || (params->itvl_min > BLE_HCI_CONN_ITVL_MAX)) {
    return BLE_ERR_INV_HCI_CMD_PARMS;
  }
  /* Check connection interval max */
  if ((params->itvl_max < BLE_HCI_CONN_ITVL_MIN) || (params->itvl_max > BLE_HCI_CONN_ITVL_MAX) || (params->itvl_max < params->itvl_min)) {
    return BLE_ERR_INV_HCI_CMD_PARMS;
  }

  /* Check connection latency */
  if (params->latency > BLE_HCI_CONN_LATENCY_MAX) {
    return BLE_ERR_INV_HCI_CMD_PARMS;
  }

  /* Check supervision timeout */
  if ((params->supervision_timeout < BLE_HCI_CONN_SPVN_TIMEOUT_MIN) || (params->supervision_timeout > BLE_HCI_CONN_SPVN_TIMEOUT_MAX)) {
    return BLE_ERR_INV_HCI_CMD_PARMS;
  }

  /* Check connection event length */
  if (params->min_ce_len > params->max_ce_len) {
    return BLE_ERR_INV_HCI_CMD_PARMS;
  }

  return 0;
}

/**
 * @brief Converts a return code from the NimBLE stack to a text string.
 * @param [in] rc The return code to convert.
 * @return A string representation of the return code.
 */
const char *BLEUtils::returnCodeToString(int rc) {
#if defined(CONFIG_NIMBLE_ENABLE_RETURN_CODE_TEXT)
  switch (rc) {
    case 0:                                             return "SUCCESS";
    case BLE_HS_EAGAIN:                                 return "Temporary failure; try again.";
    case BLE_HS_EALREADY:                               return "Operation already in progress or completed.";
    case BLE_HS_EINVAL:                                 return "One or more arguments are invalid.";
    case BLE_HS_EMSGSIZE:                               return "The provided buffer is too small.";
    case BLE_HS_ENOENT:                                 return "No entry matching the specified criteria.";
    case BLE_HS_ENOMEM:                                 return "Operation failed due to resource exhaustion.";
    case BLE_HS_ENOTCONN:                               return "No open connection with the specified handle.";
    case BLE_HS_ENOTSUP:                                return "Operation disabled at compile time.";
    case BLE_HS_EAPP:                                   return "Application callback behaved unexpectedly.";
    case BLE_HS_EBADDATA:                               return "Command from peer is invalid.";
    case BLE_HS_EOS:                                    return "Mynewt OS error.";
    case BLE_HS_ECONTROLLER:                            return "Event from controller is invalid.";
    case BLE_HS_ETIMEOUT:                               return "Operation timed out.";
    case BLE_HS_EDONE:                                  return "Operation completed successfully.";
    case BLE_HS_EBUSY:                                  return "Operation cannot be performed until procedure completes.";
    case BLE_HS_EREJECT:                                return "Peer rejected a connection parameter update request.";
    case BLE_HS_EUNKNOWN:                               return "Unexpected failure; catch all.";
    case BLE_HS_EROLE:                                  return "Operation requires different role (e.g., central vs. peripheral).";
    case BLE_HS_ETIMEOUT_HCI:                           return "HCI request timed out; controller unresponsive.";
    case BLE_HS_ENOMEM_EVT:                             return "Controller failed to send event due to memory exhaustion (combined host-controller only).";
    case BLE_HS_ENOADDR:                                return "Operation requires an identity address but none configured.";
    case BLE_HS_ENOTSYNCED:                             return "Attempt to use the host before it is synced with controller.";
    case BLE_HS_EAUTHEN:                                return "Insufficient authentication.";
    case BLE_HS_EAUTHOR:                                return "Insufficient authorization.";
    case BLE_HS_EENCRYPT:                               return "Insufficient encryption level.";
    case BLE_HS_EENCRYPT_KEY_SZ:                        return "Insufficient key size.";
    case BLE_HS_ESTORE_CAP:                             return "Storage at capacity.";
    case BLE_HS_ESTORE_FAIL:                            return "Storage IO error.";
    case BLE_HS_EPREEMPTED:                             return "Operation was preempted.";
    case BLE_HS_EDISABLED:                              return "Operation disabled.";
    case BLE_HS_ESTALLED:                               return "Operation stalled.";
    case (0x0100 + BLE_ATT_ERR_INVALID_HANDLE):         return "The attribute handle given was not valid on this server.";
    case (0x0100 + BLE_ATT_ERR_READ_NOT_PERMITTED):     return "The attribute cannot be read.";
    case (0x0100 + BLE_ATT_ERR_WRITE_NOT_PERMITTED):    return "The attribute cannot be written.";
    case (0x0100 + BLE_ATT_ERR_INVALID_PDU):            return "The attribute PDU was invalid.";
    case (0x0100 + BLE_ATT_ERR_INSUFFICIENT_AUTHEN):    return "The attribute requires authentication before it can be read or written.";
    case (0x0100 + BLE_ATT_ERR_REQ_NOT_SUPPORTED):      return "Attribute server does not support the request received from the client.";
    case (0x0100 + BLE_ATT_ERR_INVALID_OFFSET):         return "Offset specified was past the end of the attribute.";
    case (0x0100 + BLE_ATT_ERR_INSUFFICIENT_AUTHOR):    return "The attribute requires authorization before it can be read or written.";
    case (0x0100 + BLE_ATT_ERR_PREPARE_QUEUE_FULL):     return "Too many prepare writes have been queued.";
    case (0x0100 + BLE_ATT_ERR_ATTR_NOT_FOUND):         return "No attribute found within the given attribute handle range.";
    case (0x0100 + BLE_ATT_ERR_ATTR_NOT_LONG):          return "The attribute cannot be read or written using the Read Blob Request.";
    case (0x0100 + BLE_ATT_ERR_INSUFFICIENT_KEY_SZ):    return "The Encryption Key Size used for encrypting this link is insufficient.";
    case (0x0100 + BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN): return "The attribute value length is invalid for the operation.";
    case (0x0100 + BLE_ATT_ERR_UNLIKELY):               return "The attribute request has encountered an error that was unlikely, could not be completed as requested.";
    case (0x0100 + BLE_ATT_ERR_INSUFFICIENT_ENC):       return "The attribute requires encryption before it can be read or written.";
    case (0x0100 + BLE_ATT_ERR_UNSUPPORTED_GROUP):
      return "The attribute type is not a supported grouping attribute as defined by a higher layer specification.";
    case (0x0100 + BLE_ATT_ERR_INSUFFICIENT_RES):         return "Insufficient Resources to complete the request.";
    case (0x0200 + BLE_ERR_UNKNOWN_HCI_CMD):              return "Unknown HCI Command";
    case (0x0200 + BLE_ERR_UNK_CONN_ID):                  return "Unknown Connection Identifier";
    case (0x0200 + BLE_ERR_HW_FAIL):                      return "Hardware Failure";
    case (0x0200 + BLE_ERR_PAGE_TMO):                     return "Page Timeout";
    case (0x0200 + BLE_ERR_AUTH_FAIL):                    return "Authentication Failure";
    case (0x0200 + BLE_ERR_PINKEY_MISSING):               return "PIN or Key Missing";
    case (0x0200 + BLE_ERR_MEM_CAPACITY):                 return "Memory Capacity Exceeded";
    case (0x0200 + BLE_ERR_CONN_SPVN_TMO):                return "Connection Timeout";
    case (0x0200 + BLE_ERR_CONN_LIMIT):                   return "Connection Limit Exceeded";
    case (0x0200 + BLE_ERR_SYNCH_CONN_LIMIT):             return "Synchronous Connection Limit To A Device Exceeded";
    case (0x0200 + BLE_ERR_ACL_CONN_EXISTS):              return "ACL Connection Already Exists";
    case (0x0200 + BLE_ERR_CMD_DISALLOWED):               return "Command Disallowed";
    case (0x0200 + BLE_ERR_CONN_REJ_RESOURCES):           return "Connection Rejected due to Limited Resources";
    case (0x0200 + BLE_ERR_CONN_REJ_SECURITY):            return "Connection Rejected Due To Security Reasons";
    case (0x0200 + BLE_ERR_CONN_REJ_BD_ADDR):             return "Connection Rejected due to Unacceptable BD_ADDR";
    case (0x0200 + BLE_ERR_CONN_ACCEPT_TMO):              return "Connection Accept Timeout Exceeded";
    case (0x0200 + BLE_ERR_UNSUPPORTED):                  return "Unsupported Feature or Parameter Value";
    case (0x0200 + BLE_ERR_INV_HCI_CMD_PARMS):            return "Invalid HCI Command Parameters";
    case (0x0200 + BLE_ERR_REM_USER_CONN_TERM):           return "Remote User Terminated Connection";
    case (0x0200 + BLE_ERR_RD_CONN_TERM_RESRCS):          return "Remote Device Terminated Connection due to Low Resources";
    case (0x0200 + BLE_ERR_RD_CONN_TERM_PWROFF):          return "Remote Device Terminated Connection due to Power Off";
    case (0x0200 + BLE_ERR_CONN_TERM_LOCAL):              return "Connection Terminated By Local Host";
    case (0x0200 + BLE_ERR_REPEATED_ATTEMPTS):            return "Repeated Attempts";
    case (0x0200 + BLE_ERR_NO_PAIRING):                   return "Pairing Not Allowed";
    case (0x0200 + BLE_ERR_UNK_LMP):                      return "Unknown LMP PDU";
    case (0x0200 + BLE_ERR_UNSUPP_REM_FEATURE):           return "Unsupported Remote Feature / Unsupported LMP Feature";
    case (0x0200 + BLE_ERR_SCO_OFFSET):                   return "SCO Offset Rejected";
    case (0x0200 + BLE_ERR_SCO_ITVL):                     return "SCO Interval Rejected";
    case (0x0200 + BLE_ERR_SCO_AIR_MODE):                 return "SCO Air Mode Rejected";
    case (0x0200 + BLE_ERR_INV_LMP_LL_PARM):              return "Invalid LMP Parameters / Invalid LL Parameters";
    case (0x0200 + BLE_ERR_UNSPECIFIED):                  return "Unspecified Error";
    case (0x0200 + BLE_ERR_UNSUPP_LMP_LL_PARM):           return "Unsupported LMP Parameter Value / Unsupported LL Parameter Value";
    case (0x0200 + BLE_ERR_NO_ROLE_CHANGE):               return "Role Change Not Allowed";
    case (0x0200 + BLE_ERR_LMP_LL_RSP_TMO):               return "LMP Response Timeout / LL Response Timeout";
    case (0x0200 + BLE_ERR_LMP_COLLISION):                return "LMP Error Transaction Collision";
    case (0x0200 + BLE_ERR_LMP_PDU):                      return "LMP PDU Not Allowed";
    case (0x0200 + BLE_ERR_ENCRYPTION_MODE):              return "Encryption Mode Not Acceptable";
    case (0x0200 + BLE_ERR_LINK_KEY_CHANGE):              return "Link Key cannot be Changed";
    case (0x0200 + BLE_ERR_UNSUPP_QOS):                   return "Requested QoS Not Supported";
    case (0x0200 + BLE_ERR_INSTANT_PASSED):               return "Instant Passed";
    case (0x0200 + BLE_ERR_UNIT_KEY_PAIRING):             return "Pairing With Unit Key Not Supported";
    case (0x0200 + BLE_ERR_DIFF_TRANS_COLL):              return "Different Transaction Collision";
    case (0x0200 + BLE_ERR_QOS_PARM):                     return "QoS Unacceptable Parameter";
    case (0x0200 + BLE_ERR_QOS_REJECTED):                 return "QoS Rejected";
    case (0x0200 + BLE_ERR_CHAN_CLASS):                   return "Channel Classification Not Supported";
    case (0x0200 + BLE_ERR_INSUFFICIENT_SEC):             return "Insufficient Security";
    case (0x0200 + BLE_ERR_PARM_OUT_OF_RANGE):            return "Parameter Out Of Mandatory Range";
    case (0x0200 + BLE_ERR_PENDING_ROLE_SW):              return "Role Switch Pending";
    case (0x0200 + BLE_ERR_RESERVED_SLOT):                return "Reserved Slot Violation";
    case (0x0200 + BLE_ERR_ROLE_SW_FAIL):                 return "Role Switch Failed";
    case (0x0200 + BLE_ERR_INQ_RSP_TOO_BIG):              return "Extended Inquiry Response Too Large";
    case (0x0200 + BLE_ERR_SEC_SIMPLE_PAIR):              return "Secure Simple Pairing Not Supported By Host";
    case (0x0200 + BLE_ERR_HOST_BUSY_PAIR):               return "Host Busy - Pairing";
    case (0x0200 + BLE_ERR_CONN_REJ_CHANNEL):             return "Connection Rejected, No Suitable Channel Found";
    case (0x0200 + BLE_ERR_CTLR_BUSY):                    return "Controller Busy";
    case (0x0200 + BLE_ERR_CONN_PARMS):                   return "Unacceptable Connection Parameters";
    case (0x0200 + BLE_ERR_DIR_ADV_TMO):                  return "Directed Advertising Timeout";
    case (0x0200 + BLE_ERR_CONN_TERM_MIC):                return "Connection Terminated due to MIC Failure";
    case (0x0200 + BLE_ERR_CONN_ESTABLISHMENT):           return "Connection Failed to be Established";
    case (0x0200 + BLE_ERR_MAC_CONN_FAIL):                return "MAC Connection Failed";
    case (0x0200 + BLE_ERR_COARSE_CLK_ADJ):               return "Coarse Clock Adjustment Rejected";
    case (0x0300 + BLE_L2CAP_SIG_ERR_CMD_NOT_UNDERSTOOD): return "Invalid or unsupported incoming L2CAP sig command.";
    case (0x0300 + BLE_L2CAP_SIG_ERR_MTU_EXCEEDED):       return "Incoming packet too large.";
    case (0x0300 + BLE_L2CAP_SIG_ERR_INVALID_CID):        return "No channel with specified ID.";
    case (0x0400 + BLE_SM_ERR_PASSKEY):                   return "The user input of passkey failed, for example, the user canceled the operation.";
    case (0x0400 + BLE_SM_ERR_OOB):                       return "The OOB data is not available.";
    case (0x0400 + BLE_SM_ERR_AUTHREQ):
      return "The pairing procedure cannot be performed as authentication requirements cannot be met due to IO capabilities of one or both devices.";
    case (0x0400 + BLE_SM_ERR_CONFIRM_MISMATCH): return "The confirm value does not match the calculated compare value.";
    case (0x0400 + BLE_SM_ERR_PAIR_NOT_SUPP):    return "Pairing is not supported by the device.";
    case (0x0400 + BLE_SM_ERR_ENC_KEY_SZ):       return "The resultant encryption key size is insufficient for the security requirements of this device.";
    case (0x0400 + BLE_SM_ERR_CMD_NOT_SUPP):     return "The SMP command received is not supported on this device.";
    case (0x0400 + BLE_SM_ERR_UNSPECIFIED):      return "Pairing failed due to an unspecified reason.";
    case (0x0400 + BLE_SM_ERR_REPEATED):
      return "Pairing or authentication procedure disallowed, too little time has elapsed since last pairing request or security request.";
    case (0x0400 + BLE_SM_ERR_INVAL):   return "Command length is invalid or that a parameter is outside of the specified range.";
    case (0x0400 + BLE_SM_ERR_DHKEY):   return "DHKey Check value received doesn't match the one calculated by the local device.";
    case (0x0400 + BLE_SM_ERR_NUMCMP):  return "Confirm values in the numeric comparison protocol do not match.";
    case (0x0400 + BLE_SM_ERR_ALREADY): return "Pairing over the LE transport failed - Pairing Request sent over the BR/EDR transport in process.";
    case (0x0400 + BLE_SM_ERR_CROSS_TRANS):
      return "BR/EDR Link Key generated on the BR/EDR transport cannot be used to derive and distribute keys for the LE transport.";
    case (0x0500 + BLE_SM_ERR_PASSKEY): return "The user input of passkey failed or the user canceled the operation.";
    case (0x0500 + BLE_SM_ERR_OOB):     return "The OOB data is not available.";
    case (0x0500 + BLE_SM_ERR_AUTHREQ):
      return "The pairing procedure cannot be performed as authentication requirements cannot be met due to IO capabilities of one or both devices.";
    case (0x0500 + BLE_SM_ERR_CONFIRM_MISMATCH): return "The confirm value does not match the calculated compare value.";
    case (0x0500 + BLE_SM_ERR_PAIR_NOT_SUPP):    return "Pairing is not supported by the device.";
    case (0x0500 + BLE_SM_ERR_ENC_KEY_SZ):       return "The resultant encryption key size is insufficient for the security requirements of this device.";
    case (0x0500 + BLE_SM_ERR_CMD_NOT_SUPP):     return "The SMP command received is not supported on this device.";
    case (0x0500 + BLE_SM_ERR_UNSPECIFIED):      return "Pairing failed due to an unspecified reason.";
    case (0x0500 + BLE_SM_ERR_REPEATED):
      return "Pairing or authentication procedure is disallowed because too little time has elapsed since last pairing request or security request.";
    case (0x0500 + BLE_SM_ERR_INVAL): return "Command length is invalid or a parameter is outside of the specified range.";
    case (0x0500 + BLE_SM_ERR_DHKEY):
      return "Indicates to the remote device that the DHKey Check value received doesn't match the one calculated by the local device.";
    case (0x0500 + BLE_SM_ERR_NUMCMP):  return "Confirm values in the numeric comparison protocol do not match.";
    case (0x0500 + BLE_SM_ERR_ALREADY): return "Pairing over the LE transport failed - Pairing Request sent over the BR/EDR transport in process.";
    case (0x0500 + BLE_SM_ERR_CROSS_TRANS):
      return "BR/EDR Link Key generated on the BR/EDR transport cannot be used to derive and distribute keys for the LE transport.";
    default: return "Unknown";
  }
#else   // #if defined(CONFIG_NIMBLE_ENABLE_RETURN_CODE_TEXT)
  return "";
#endif  // #if defined(CONFIG_NIMBLE_ENABLE_RETURN_CODE_TEXT)
}

/**
 * @brief Utility function to log the gap event info.
 * @param [in] event A pointer to the gap event structure.
 * @param [in] arg Unused.
 */
void BLEUtils::dumpGapEvent(ble_gap_event *event, void *arg) {
#if defined(CONFIG_NIMBLE_ENABLE_GAP_EVENT_CODE_TEXT)
  log_d("Received a GAP event: %s", gapEventToString(event->type));
#endif
}

/**
 * @brief Convert a GAP event type to a string representation.
 * @param [in] eventType The type of event.
 * @return A string representation of the event type.
 */
const char *BLEUtils::gapEventToString(uint8_t eventType) {
#if defined(CONFIG_NIMBLE_ENABLE_GAP_EVENT_CODE_TEXT)
  switch (eventType) {
    case BLE_GAP_EVENT_CONNECT:  //0
      return "BLE_GAP_EVENT_CONNECT ";

    case BLE_GAP_EVENT_DISCONNECT:  //1
      return "BLE_GAP_EVENT_DISCONNECT";

    case BLE_GAP_EVENT_CONN_UPDATE:  //3
      return "BLE_GAP_EVENT_CONN_UPDATE";

    case BLE_GAP_EVENT_CONN_UPDATE_REQ:  //4
      return "BLE_GAP_EVENT_CONN_UPDATE_REQ";

    case BLE_GAP_EVENT_L2CAP_UPDATE_REQ:  //5
      return "BLE_GAP_EVENT_L2CAP_UPDATE_REQ";

    case BLE_GAP_EVENT_TERM_FAILURE:  //6
      return "BLE_GAP_EVENT_TERM_FAILURE";

    case BLE_GAP_EVENT_DISC:  //7
      return "BLE_GAP_EVENT_DISC";

    case BLE_GAP_EVENT_DISC_COMPLETE:  //8
      return "BLE_GAP_EVENT_DISC_COMPLETE";

    case BLE_GAP_EVENT_ADV_COMPLETE:  //9
      return "BLE_GAP_EVENT_ADV_COMPLETE";

    case BLE_GAP_EVENT_ENC_CHANGE:  //10
      return "BLE_GAP_EVENT_ENC_CHANGE";

    case BLE_GAP_EVENT_PASSKEY_ACTION:  //11
      return "BLE_GAP_EVENT_PASSKEY_ACTION";

    case BLE_GAP_EVENT_NOTIFY_RX:  //12
      return "BLE_GAP_EVENT_NOTIFY_RX";

    case BLE_GAP_EVENT_NOTIFY_TX:  //13
      return "BLE_GAP_EVENT_NOTIFY_TX";

    case BLE_GAP_EVENT_SUBSCRIBE:  //14
      return "BLE_GAP_EVENT_SUBSCRIBE";

    case BLE_GAP_EVENT_MTU:  //15
      return "BLE_GAP_EVENT_MTU";

    case BLE_GAP_EVENT_IDENTITY_RESOLVED:  //16
      return "BLE_GAP_EVENT_IDENTITY_RESOLVED";

    case BLE_GAP_EVENT_REPEAT_PAIRING:  //17
      return "BLE_GAP_EVENT_REPEAT_PAIRING";

    case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE:  //18
      return "BLE_GAP_EVENT_PHY_UPDATE_COMPLETE";

    case BLE_GAP_EVENT_EXT_DISC:  //19
      return "BLE_GAP_EVENT_EXT_DISC";

    case BLE_GAP_EVENT_AUTHORIZE:  //32
      return "BLE_GAP_EVENT_AUTHORIZE";
#ifdef BLE_GAP_EVENT_PERIODIC_SYNC     // IDF 4.0 does not support these
    case BLE_GAP_EVENT_PERIODIC_SYNC:  //20
      return "BLE_GAP_EVENT_PERIODIC_SYNC";

    case BLE_GAP_EVENT_PERIODIC_REPORT:  //21
      return "BLE_GAP_EVENT_PERIODIC_REPORT";

    case BLE_GAP_EVENT_PERIODIC_SYNC_LOST:  //22
      return "BLE_GAP_EVENT_PERIODIC_SYNC_LOST";

    case BLE_GAP_EVENT_SCAN_REQ_RCVD:  //23
      return "BLE_GAP_EVENT_SCAN_REQ_RCVD";
#endif
    default: log_d("gapEventToString: Unknown event type %d 0x%02x", eventType, eventType); return "Unknown event type";
  }
#else   // #if defined(CONFIG_NIMBLE_ENABLE_GAP_EVENT_CODE_TEXT)
  return "";
#endif  // #if defined(CONFIG_NIMBLE_ENABLE_GAP_EVENT_CODE_TEXT)
}  // gapEventToString

/**
 * @brief Convert characteristic properties into a string representation.
 * @param [in] prop Characteristic properties.
 * @return A string representation of characteristic properties.
 */
String BLEUtils::characteristicPropertiesToString(uint8_t prop) {
  String res = "broadcast: ";
  res += ((prop & BLE_GATT_CHR_PROP_BROADCAST) ? "1" : "0");
  res += ", read: ";
  res += ((prop & BLE_GATT_CHR_PROP_READ) ? "1" : "0");
  res += ", write_nr: ";
  res += ((prop & BLE_GATT_CHR_PROP_WRITE_NO_RSP) ? "1" : "0");
  res += ", write: ";
  res += ((prop & BLE_GATT_CHR_PROP_WRITE) ? "1" : "0");
  res += ", notify: ";
  res += ((prop & BLE_GATT_CHR_PROP_NOTIFY) ? "1" : "0");
  res += ", indicate: ";
  res += ((prop & BLE_GATT_CHR_PROP_INDICATE) ? "1" : "0");
  res += ", auth_sign_write: ";
  res += ((prop & BLE_GATT_CHR_PROP_AUTH_SIGN_WRITE) ? "1" : "0");
  res += ", extended: ";
  res += ((prop & BLE_GATT_CHR_PROP_EXTENDED) ? "1" : "0");
  return res;
}  // characteristicPropertiesToString

/**
 * @brief Blocks the calling task until released or timeout.
 * @param [in] taskData A pointer to the task data structure.
 * @param [in] timeout The time to wait in milliseconds.
 * @return True if the task completed, false if the timeout was reached.
 */
bool BLEUtils::taskWait(const BLETaskData &taskData, uint32_t timeout) {
  ble_npl_time_t ticks;
  if (timeout == BLE_NPL_TIME_FOREVER) {
    ticks = BLE_NPL_TIME_FOREVER;
  } else {
    ble_npl_time_ms_to_ticks(timeout, &ticks);
  }

  uint32_t notificationValue;
  xTaskNotifyWait(0, TASK_BLOCK_BIT, &notificationValue, 0);
  if (notificationValue & TASK_BLOCK_BIT) {
    return true;
  }

  return xTaskNotifyWait(0, TASK_BLOCK_BIT, nullptr, ticks) == pdTRUE;
}  // taskWait

/**
 * @brief Release a task.
 * @param [in] taskData A pointer to the task data structure.
 * @param [in] flags A return value to set in the task data structure.
 */
void BLEUtils::taskRelease(const BLETaskData &taskData, int flags) {
  taskData.m_flags = flags;
  if (taskData.m_pHandle != nullptr) {
    xTaskNotify(static_cast<TaskHandle_t>(taskData.m_pHandle), TASK_BLOCK_BIT, eSetBits);
  }
}  // taskRelease

#endif  // CONFIG_NIMBLE_ENABLED

#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */
#endif /* SOC_BLE_SUPPORTED || CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE */
