diff --git a/include/bluetooth/services/ble_hids.h b/include/bluetooth/services/ble_hids.h new file mode 100644 index 0000000000..4302f5ae4e --- /dev/null +++ b/include/bluetooth/services/ble_hids.h @@ -0,0 +1,590 @@ +/* + * Copyright (c) 2012 - 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/** @file + * + * @defgroup ble_hids Human Interface Device Service + * @{ + * @brief Human Interface Device Service. + */ +#ifndef BLE_HIDS_H__ +#define BLE_HIDS_H__ + +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Define a HID service instance. + * + * Define a HID service instance and register it as a Bluetooth event observer. + */ +#define BLE_HIDS_DEF(_name) \ + static struct ble_hids _name; \ + NRF_SDH_BLE_OBSERVER(_name##_obs, ble_hids_on_ble_evt, &_name, 0) + +/** + * @brief HID boot keyboard input report maximum size, in bytes. + */ +#define BLE_HIDS_BOOT_KB_INPUT_REP_MAX_SIZE 8 +/** + * @brief HID boot keyboard output report maximum size, in bytes. + */ +#define BLE_HIDS_BOOT_KB_OUTPUT_REP_MAX_SIZE 1 +/** + * @brief HID boot mouse input report maximum size, in bytes. + */ +#define BLE_HIDS_BOOT_MOUSE_INPUT_REP_MAX_SIZE 8 + +/** + * @brief HID report types as defined in the Report Reference Characteristic descriptor. + */ +enum ble_hids_report_type { + /** + * @brief Reserved. + */ + BLE_HIDS_REPORT_TYPE_RESERVED = 0x00, + /** + * @brief Input report. + */ + BLE_HIDS_REPORT_TYPE_INPUT = 0x01, + /** + * @brief Output report. + */ + BLE_HIDS_REPORT_TYPE_OUTPUT = 0x02, + /** + * @brief Feature report. + */ + BLE_HIDS_REPORT_TYPE_FEATURE = 0x03 +}; + +/** + * @brief BLE HID service boot keyboard input report. + */ +struct ble_hids_boot_keyboard_input_report { + /** + * @brief Key modifier. + */ + enum __packed { + KEY_LEFT_CTRL = 0x01, + KEY_LEFT_SHIFT = 0x02, + KEY_LEFT_ALT = 0x04, + KEY_LEFT_GUI = 0x08, + KEY_RIGHT_CTRL = 0x10, + KEY_RIGHT_SHIFT = 0x20, + KEY_RIGHT_ALT = 0x40, + KEY_RIGHT_GUI = 0x80, + } modifier; + /** + * @brief Reserved, zero. + */ + uint8_t reserved; + /** + * @brief Key codes. + */ + uint8_t keycode[6]; +}; +BUILD_ASSERT(sizeof(struct ble_hids_boot_keyboard_input_report) == + BLE_HIDS_BOOT_KB_INPUT_REP_MAX_SIZE); + +/** + * @brief BLE HID service boot mouse input report. + */ +struct ble_hids_boot_mouse_input_report { + /** + * @brief Buttons mask. + */ + uint8_t buttons; + /** + * @brief Delta X. + */ + int8_t delta_x; + /** + * @brief Delta Y. + */ + int8_t delta_y; + /** + * @brief Optional data. + */ + uint8_t data[5]; +}; +BUILD_ASSERT(sizeof(struct ble_hids_boot_mouse_input_report) == + BLE_HIDS_BOOT_MOUSE_INPUT_REP_MAX_SIZE); + +/** + * @brief HID service characteristic ID. + */ +struct ble_hids_char_id { + /** + * @brief Characteristic UUID. + */ + uint16_t uuid; + /** + * @brief Type of report. + * + * Only used when @ref uuid is BLE_UUID_REPORT_CHAR. + */ + enum ble_hids_report_type rep_type; + /** + * @brief Index of the characteristic + * + * Only used when @ref uuid is BLE_UUID_REPORT_CHAR. + */ + uint8_t rep_index; +}; + +/** + * @brief HID Service event type. + */ +enum ble_hids_evt_type { + /** + * @brief Suspend command received. + */ + BLE_HIDS_EVT_HOST_SUSP, + /** + * @brief Exit suspend command received. + */ + BLE_HIDS_EVT_HOST_EXIT_SUSP, + /** + * @brief Notification enabled. + */ + BLE_HIDS_EVT_NOTIF_ENABLED, + /** + * @brief Notification disabled. + */ + BLE_HIDS_EVT_NOTIF_DISABLED, + /** + * @brief A new value has been written to an Report characteristic. + */ + BLE_HIDS_EVT_REP_CHAR_WRITE, + /** + * @brief Boot mode entered. + */ + BLE_HIDS_EVT_BOOT_MODE_ENTERED, + /** + * @brief Report mode entered. + */ + BLE_HIDS_EVT_REPORT_MODE_ENTERED, + /** + * @brief Read with response. + */ + BLE_HIDS_EVT_REPORT_READ +}; + +/** + * @brief HID service event. + */ +struct ble_hids_evt { + /** + * @brief Event type. + */ + enum ble_hids_evt_type evt_type; + /** + * @brief BLE event. + */ + const ble_evt_t *ble_evt; + /** + * @brief Event parameters. + */ + union { + /** + * @brief Parameters for + * - @ref BLE_HIDS_EVT_NOTIF_ENABLED + * - @ref BLE_HIDS_EVT_NOTIF_DISABLED + */ + struct { + /** + * @brief Characteristic ID. + */ + struct ble_hids_char_id char_id; + } notification; + /** + * @brief Parameters for @ref BLE_HIDS_EVT_REP_CHAR_WRITE. + */ + struct { + /** + * @brief Characteristic ID. + */ + struct ble_hids_char_id char_id; + /** + * @brief Offset of the write operation. + */ + uint16_t offset; + /** + * @brief Length of the write operation. + */ + uint16_t len; + /** + * @brief Incoming data. + */ + const void *data; + } char_write; + /** + * @brief Parameters for @ref BLE_HIDS_EVT_REPORT_READ. + */ + struct { + /** + * @brief Characteristic ID. + */ + struct ble_hids_char_id char_id; + } char_auth_read; + } params; +}; + +/** + * @brief HID report characteristic configuration. + */ +struct ble_hids_report_config { + /** + * @brief Report type. + */ + enum ble_hids_report_type report_type; + /** + * @brief Report ID. + */ + uint8_t report_id; + /** + * @brief Characteristic length. + */ + uint16_t len; + /** + * @brief Security requirements for Report characteristic. + */ + struct { + /** + * @brief Security requirements for Read operations. + */ + ble_gap_conn_sec_mode_t read; + /** + * @brief Security requirements for Write operations. + */ + ble_gap_conn_sec_mode_t write; + /** + * @brief Security requirements for CCCD Write operations. + */ + ble_gap_conn_sec_mode_t cccd_write; + } sec; +}; + +/** + * @brief HID Report characteristic. + */ +struct ble_hids_report { + /** + * @brief Report characteristic handles. + */ + ble_gatts_char_handles_t char_handles; + /** + * @brief Report reference descriptor handle. + */ + uint16_t ref_handle; + /** + * @brief Maximum report length. + */ + uint16_t max_len; +}; + +/* Forward declaration */ +struct ble_hids; + +/** + * @brief HID service event handler type. + */ +typedef void (*ble_hids_evt_handler_t)(struct ble_hids *hids, const struct ble_hids_evt *evt); + +/** + * @brief HID service configuration. + */ +struct ble_hids_config { + /** + * @brief HID service event handler. + */ + ble_hids_evt_handler_t evt_handler; + /** + * @brief Input report characteristic configuration. + */ + const struct ble_hids_report_config *input_report; + /** + * @brief Output report characteristic configuration. + */ + const struct ble_hids_report_config *output_report; + /** + * @brief Feature report characteristic configuration. + */ + const struct ble_hids_report_config *feature_report; + /** + * @brief Array of services to include in HID service. + */ + uint16_t *included_services_array; + /** + * @brief Number of Input Report characteristics in @ref input_report. + */ + uint8_t input_report_count; + /** + * @brief Number of Output Report characteristics in @ref output_report. + */ + uint8_t output_report_count; + /** + * @brief Number of Feature Report characteristics in @ref feature_report. + */ + uint8_t feature_report_count; + /** + * @brief Number of services to include in HID service. + */ + uint8_t included_services_count; + /** + * @brief HID Report Map characteristic configuration. + */ + struct { + /** + * @brief Report map data. + */ + uint8_t *data; + /** + * @brief Length of report map data. + */ + uint16_t len; + /** + * @brief External Report Reference descriptors (optional). + */ + const ble_uuid_t *ext_rep_ref; + /** + * @brief Number of External Report Reference descriptors in @ref ext_rep_ref. + */ + uint8_t ext_rep_ref_count; + /** + * @brief Security requirements. + */ + struct { + /** + * @brief Security requirements for the Read operation. + */ + ble_gap_conn_sec_mode_t read; + } sec; + } report_map; + /** + * @brief HID information characteristic configuration. + */ + struct { + /** + * @brief HID version specification. + * + * Binary coded decimal. + */ + uint16_t bcd_hid; + /** + * @brief Country code, if device is localized. + */ + uint8_t b_country_code; + /** + * @brief HID information flags. + */ + struct { + /** + * @brief Device is normally connectable. + */ + uint8_t normally_connectable: 1; + /** + * @brief Device can be waked remotely. + */ + uint8_t remote_wake: 1; + /** + * @brief Reserved. + */ + uint8_t reserved: 6; + } bcd_flags; + /** + * @brief Security requirements for HID information characteristic. + */ + struct { + /** + * @brief Security requirements for the Read operation. + */ + ble_gap_conn_sec_mode_t read; + } sec; + } hid_information; +}; + +/** + * @brief HID service structure. + */ +struct ble_hids { + /** + * @brief HID Service event handler. + */ + ble_hids_evt_handler_t evt_handler; + /** + * @brief Handle of HID Service. + */ + uint16_t service_handle; + /** + * @brief Control point characteristic handles. + */ + ble_gatts_char_handles_t control_point_handles; + /** + * @brief Protocol Mode characteristic handles. + */ + ble_gatts_char_handles_t protocol_mode_handles; + /** + * @brief Report Map characteristic handles. + */ + ble_gatts_char_handles_t rep_map_handles; + /** + * @brief HID information characteristic handles. + */ + ble_gatts_char_handles_t hid_information_handles; +#if CONFIG_BLE_HIDS_BOOT_KEYBOARD + /** + * @brief Boot Keyboard Input Report characteristic handles. + */ + ble_gatts_char_handles_t boot_kb_inp_rep_handles; + /** + * @brief Boot Keyboard Output Report characteristic handles. + */ + ble_gatts_char_handles_t boot_kb_outp_rep_handles; +#endif +#if CONFIG_BLE_HIDS_BOOT_MOUSE + /** + * @brief Boot Mouse Input Report characteristic handles. + */ + ble_gatts_char_handles_t boot_mouse_inp_rep_handles; +#endif + /** + * @brief Number of input reports. + */ + uint8_t input_report_count; + /** + * @brief Number of output reports. + */ + uint8_t output_report_count; + /** + * @brief Number of feature reports. + */ + uint8_t feature_report_count; + /** + * @brief Input reports. + */ + struct ble_hids_report input_report[CONFIG_BLE_HIDS_MAX_INPUT_REP]; + /** + * @brief Output reports. + */ + struct ble_hids_report output_report[CONFIG_BLE_HIDS_MAX_OUTPUT_REP]; + /** + * @brief Feature reports. + */ + struct ble_hids_report feature_report[CONFIG_BLE_HIDS_MAX_FEATURE_REP]; +}; + +/** + * @brief HID service event handler for SoftDevice BLE events. + * + * @param ble_evt SoftDevice BLE event. + * @param ctx Context. + */ +void ble_hids_on_ble_evt(const ble_evt_t *ble_evt, void *ctx); + +/** + * @brief Set the event handler for HID service. + * + * @param hids HID service. + * @param handler HID service event handler. + * + * @retval 0 On success. + * @retval NRF_ERROR_NULL If @p hids or @p handler are @c NULL. + */ +uint32_t ble_hids_event_handler_set(struct ble_hids *hids, ble_hids_evt_handler_t handler); + +/** + * @brief Initialize HID service. + * + * @param hids HID service. + * @param hids_config HID service configuration. + * + * @retval 0 On success. + * @retval NRF_ERROR_NULL If @p hids or @p hids_config are @c NULL. + * @retval NRF_ERROR_INVALID_PARAM Invalid parameters. + */ +uint32_t ble_hids_init(struct ble_hids *hids, const struct ble_hids_config *hids_config); + +/** + * @brief Send an input report. + * + * @param hids HID service. + * @param conn_handle Connection handle. + * @param rep_index Index of the input report characteristic. + * @param data Input report data. + * @param len Input report data length. + * + * @retval 0 On success. + * @retval NRF_ERROR_NULL If @p hids or @p data are @c NULL. + * @retval NRF_ERROR_NOT_FOUND Unknown connection handle. + * @retval NRF_ERROR_INVALID_PARAM Report index @p rep_index is invalid. + * @retval NRF_ERROR_DATA_SIZE Report data length @p len exceeds maximum characteristic length. + */ +uint32_t ble_hids_inp_rep_send(struct ble_hids *hids, uint16_t conn_handle, uint8_t rep_index, + const void *data, uint16_t len); + +/** + * @brief Send a boot keyboard input report. + * + * @param hids HID service. + * @param conn_handle Connection handle. + * @param report Boot keyboard input report. + * + * @retval 0 On success. + * @retval NRF_ERROR_NULL If @p hids or @p report are @c NULL. + * @retval NRF_ERROR_NOT_FOUND Unknown connection handle. + */ +uint32_t ble_hids_boot_kb_inp_rep_send(struct ble_hids *hids, uint16_t conn_handle, + const struct ble_hids_boot_keyboard_input_report *report); + +/** + * @brief Send a boot mouse input report. + * + * @param hids HID service. + * @param conn_handle Connection handle. + * @param report Boot mouse input report. + * + * @retval 0 On success. + * @retval NRF_ERROR_NULL If @p hids or @p report are @c NULL. + * @retval NRF_ERROR_NOT_FOUND Unknown connection handle. + */ +uint32_t ble_hids_boot_mouse_inp_rep_send(struct ble_hids *hids, uint16_t conn_handle, + const struct ble_hids_boot_mouse_input_report *report); + +/** + * @brief Retrieve output report data. + * + * @param hids HID service. + * @param rep_index Output report index. + * @param len Length of requested data. + * @param offset Offset of the data in the report. + * @param conn_handle Connection handle. + * @param outp_rep Output report buffer. + * + * @retval 0 On success. + * @retval NRF_ERROR_NULL If @p hids or @p outp_rep are @c NULL. + * @retval NRF_ERROR_NOT_FOUND Unknown connection handle. + * @retval NRF_ERROR_INVALID_PARAM Report index @p rep_index is invalid. + * @retval NRF_ERROR_DATA_SIZE The operation exceeds the maximum characteristic length. + */ +uint32_t ble_hids_outp_rep_get(struct ble_hids *hids, uint8_t rep_index, uint16_t len, + uint8_t offset, uint16_t conn_handle, uint8_t *outp_rep); + +#ifdef __cplusplus +} +#endif + +#endif /* BLE_HIDS_H__ */ + +/** @} */ diff --git a/include/bluetooth/services/common.h b/include/bluetooth/services/common.h index 2cc874af99..e8ece9381c 100644 --- a/include/bluetooth/services/common.h +++ b/include/bluetooth/services/common.h @@ -7,7 +7,9 @@ #ifndef BLE_SERVICES_COMMON_H__ #define BLE_SERVICES_COMMON_H__ + #include +#include #include #ifdef __cplusplus @@ -34,6 +36,75 @@ static inline uint16_t is_indication_enabled(const uint8_t *gatts_write_data) .sm = ((x) >> 4) & 0xf, .lv = (x) & 0xf, \ } +static inline bool ble_gap_conn_sec_mode_equal(const ble_gap_conn_sec_mode_t *a, + const ble_gap_conn_sec_mode_t *b) +{ + return (a->sm == b->sm) && (a->lv == b->lv); +} + +/** + * @brief Set sec_mode to have no access rights. + */ +#define BLE_GAP_CONN_SEC_MODE_NO_ACCESS \ + (ble_gap_conn_sec_mode_t) \ + { \ + .sm = 0, .lv = 0 \ + } + +/** + * @brief Set sec_mode to require no protection, open link. + */ +#define BLE_GAP_CONN_SEC_MODE_OPEN \ + (ble_gap_conn_sec_mode_t) \ + { \ + .sm = 1, .lv = 1 \ + } + +/** + * @brief Set sec_mode to require encryption, but no MITM protection. + */ +#define BLE_GAP_CONN_SEC_MODE_ENC_NO_MITM \ + (ble_gap_conn_sec_mode_t) \ + { \ + .sm = 1, .lv = 2 \ + } + +/** + * @brief Set sec_mode to require encryption and MITM protection. + */ +#define BLE_GAP_CONN_SEC_MODE_ENC_WITH_MITM \ + (ble_gap_conn_sec_mode_t) \ + { \ + .sm = 1, .lv = 3 \ + } + +/** + * @brief Set sec_mode to require LESC encryption and MITM protection. + */ +#define BLE_GAP_CONN_SEC_MODE_LESC_ENC_WITH_MITM \ + (ble_gap_conn_sec_mode_t) \ + { \ + .sm = 1, .lv = 4 \ + } + +/** + * @brief Set sec_mode to require signing or encryption, no MITM protection needed. + */ +#define BLE_GAP_CONN_SEC_MODE_SIGNED_NO_MITM \ + (ble_gap_conn_sec_mode_t) \ + { \ + .sm = 2, .lv = 1 \ + } + +/** + * @brief Set sec_mode to require signing or encryption with MITM protection. + */ +#define BLE_GAP_CONN_SEC_MODE_SIGNED_WITH_MITM \ + (ble_gap_conn_sec_mode_t) \ + { \ + .sm = 2, .lv = 2 \ + } + #ifdef __cplusplus } #endif diff --git a/samples/bluetooth/ble_hids_keyboard/CMakeLists.txt b/samples/bluetooth/ble_hids_keyboard/CMakeLists.txt new file mode 100644 index 0000000000..5fcc561aba --- /dev/null +++ b/samples/bluetooth/ble_hids_keyboard/CMakeLists.txt @@ -0,0 +1,12 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(ble_hids_keyboard) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/bluetooth/ble_hids_keyboard/Kconfig b/samples/bluetooth/ble_hids_keyboard/Kconfig new file mode 100644 index 0000000000..c0a2225cfc --- /dev/null +++ b/samples/bluetooth/ble_hids_keyboard/Kconfig @@ -0,0 +1,16 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "BLE HIDS Keyboard sample" + +module=BLE_HIDS_KEYBOARD_SAMPLE +module-dep=LOG +module-str=BLE HIDS Kayboard Sample +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +endmenu # "BLE HIDS Mouse sample" + +source "Kconfig.zephyr" diff --git a/samples/bluetooth/ble_hids_keyboard/README.rst b/samples/bluetooth/ble_hids_keyboard/README.rst new file mode 100644 index 0000000000..02fbbc4126 --- /dev/null +++ b/samples/bluetooth/ble_hids_keyboard/README.rst @@ -0,0 +1,88 @@ +.. _ble_hids_keyboard_sample: + +Bluetooth: Human Interface Device Service Keyboard +################################################## + +.. contents:: + :local: + :depth: 2 + +The Peripheral HIDS keyboard sample demonstrates how to use the Human Interface Device (HID) Service to implement a keyboard input device that you can connect to your computer. + +Requirements +************ + +The sample supports the following development kits: + +.. list-table:: + :header-rows: 1 + + * - Hardware platform + - PCA + - SoftDevice + - Board target + * - `nRF54L15 DK`_ + - PCA10156 + - S115 + - bm_nrf54l15dk/nrf54l15/cpuapp/softdevice_s115 + * - `nRF54L15 DK`_ (emulating nRF54L10) + - PCA10156 + - S115 + - bm_nrf54l15dk/nrf54l10/cpuapp/softdevice_s115 + * - `nRF54L15 DK`_ (emulating nRF54L05) + - PCA10156 + - S115 + - bm_nrf54l15dk/nrf54l05/cpuapp/softdevice_s115 + +Overview +******** + +The sample uses the buttons on a development kit to simulate keys on a keyboard. +One button simulates the letter keys by generating letter keystrokes for a predefined string. +A second button simulates the Shift button and shows how to modify the letter keystrokes. +An LED displays the Caps Lock state, which can be modified by another connected keyboard. + +This sample exposes the HID GATT Service. +It uses a report map for a generic keyboard. + +User interface +************** + +Button 0: + Sends one character of the predefined input ("hello\\n") to the computer. + +Button 1: + Simulates the Shift key. + +LED 0: + Blinks with a period of two seconds with the duty cycle set to 50% when the main loop is running and the device is advertising. + +LED 1: + Lit when at least one device is connected. + +LED 2: + Indicates if Caps Lock is enabled. + +Programming the S115 SoftDevice +******************************* + +.. include:: /includes/softdevice_flash.txt + +.. _ble_hids_keyboard_sample_testing: + + +Building and running +******************** + +This sample can be found under :file:`samples/bluetooth/ble_hids_keyboard/` in the |BMshort| folder structure. + +.. include:: /includes/create_sample.txt + +.. include:: /includes/configure_and_build_sample.txt + +.. include:: /includes/program_sample.txt + +Testing +======= + +TODO diff --git a/samples/bluetooth/ble_hids_keyboard/prj.conf b/samples/bluetooth/ble_hids_keyboard/prj.conf new file mode 100644 index 0000000000..f0af147938 --- /dev/null +++ b/samples/bluetooth/ble_hids_keyboard/prj.conf @@ -0,0 +1,37 @@ +CONFIG_LOG=y +CONFIG_LOG_BACKEND_BM_UARTE=y + +CONFIG_BLE_ADV=y +CONFIG_BLE_ADV_NAME="nRF_BM_HIDS_KB" +CONFIG_BLE_ADV_EXTENDED_ADVERTISING=n +CONFIG_BLE_ADV_DIRECTED_ADVERTISING=n + +CONFIG_SOFTDEVICE=y +CONFIG_NRF_SDH=y + +# Enable RNG +CONFIG_NRF_SECURITY=y +CONFIG_MBEDTLS_PSA_CRYPTO_C=y +CONFIG_PSA_WANT_GENERATE_RANDOM=y + +# BLE connection parameter +CONFIG_BLE_CONN_PARAMS=y + +# Device information service +CONFIG_BLE_DIS=y +CONFIG_BLE_DIS_SERIAL_NUMBER="ABCD" +CONFIG_BLE_DIS_HW_REVISION="hw 54.15.0" +CONFIG_BLE_DIS_FW_REVISION="fw 17.2.0" +CONFIG_BLE_DIS_SW_REVISION="sw 1.0.0" + +# Battery service +CONFIG_BLE_BAS=y + +# Buttons and timer +CONFIG_BM_TIMER=y +CONFIG_BM_BUTTONS=y + +# HIDS service +CONFIG_BLE_HIDS=y +CONFIG_BLE_HIDS_BOOT_KEYBOARD=y +CONFIG_BLE_HIDS_LOG_LEVEL_DBG=y diff --git a/samples/bluetooth/ble_hids_keyboard/sample.yaml b/samples/bluetooth/ble_hids_keyboard/sample.yaml new file mode 100644 index 0000000000..82e538e02f --- /dev/null +++ b/samples/bluetooth/ble_hids_keyboard/sample.yaml @@ -0,0 +1,15 @@ +sample: + name: BLE HIDS Keyboard +tests: + sample.ble_hids_keyboard: + sysbuild: true + build_only: true + integration_platforms: + - bm_nrf54l15dk/nrf54l05/cpuapp/s115_softdevice + - bm_nrf54l15dk/nrf54l10/cpuapp/s115_softdevice + - bm_nrf54l15dk/nrf54l15/cpuapp/s115_softdevice + platform_allow: + - bm_nrf54l15dk/nrf54l05/cpuapp/s115_softdevice + - bm_nrf54l15dk/nrf54l10/cpuapp/s115_softdevice + - bm_nrf54l15dk/nrf54l15/cpuapp/s115_softdevice + tags: ci_build diff --git a/samples/bluetooth/ble_hids_keyboard/src/main.c b/samples/bluetooth/ble_hids_keyboard/src/main.c new file mode 100644 index 0000000000..0c5b58bd8f --- /dev/null +++ b/samples/bluetooth/ble_hids_keyboard/src/main.c @@ -0,0 +1,489 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(app, CONFIG_BLE_HIDS_KEYBOARD_SAMPLE_LOG_LEVEL); + +#define BASE_USB_HID_SPEC_VERSION 0x0101 + +/* Control key codes - required 8 of them */ +#define INPUT_REPORT_KEYS_CTRL_CODE_MIN 224 +/* Control key codes - required 8 of them */ +#define INPUT_REPORT_KEYS_CTRL_CODE_MAX 231 +/* Index of Input Report. */ +#define INPUT_REPORT_KEYS_INDEX 0 +/* Maximum length of the Input Report characteristic. */ +#define INPUT_REPORT_KEYS_MAX_LEN 8 +/* Id of reference to Keyboard Input Report. */ +#define INPUT_REP_REF_ID 0 + +/* Index of Output Report. */ +#define OUTPUT_REPORT_INDEX 0 +/* Maximum length of the Output Report characteristic. */ +#define OUTPUT_REPORT_MAX_LEN 1 +/* Id of reference to Keyboard Output Report. */ +#define OUTPUT_REP_REF_ID 0 +/* Index of Feature Report. */ +#define FEATURE_REPORT_INDEX 0 +/* Maximum length of the Feature Report characteristic. */ +#define FEATURE_REPORT_MAX_LEN 2 +/* Id of reference to Keyboard Feature Report. */ +#define FEATURE_REP_REF_ID 0 +/* CAPS LOCK bit in Output Report (based on 'LED Page (0x08)' of the + * Universal Serial Bus HID Usage Tables). + */ +#define OUTPUT_REPORT_BIT_MASK_CAPS_LOCK 0x02 + +#define BTN_PRESSED 1 + +/* HID service instance */ +BLE_HIDS_DEF(ble_hids); +/* BLE Advertising library instance */ +BLE_ADV_DEF(ble_adv); + +/* BLE Connection handle */ +static uint16_t conn_handle = BLE_CONN_HANDLE_INVALID; + +static uint8_t keys_report[INPUT_REPORT_KEYS_MAX_LEN]; + +static void on_ble_evt(const ble_evt_t *evt, void *ctx) +{ + uint32_t nrf_err; + + switch (evt->header.evt_id) { + case BLE_GAP_EVT_CONNECTED: + LOG_INF("Peer connected"); + conn_handle = evt->evt.gap_evt.conn_handle; + nrf_err = sd_ble_gatts_sys_attr_set(conn_handle, NULL, 0, 0); + if (nrf_err) { + LOG_ERR("Failed to set system attributes, nrf_error %d", nrf_err); + } + break; + case BLE_GAP_EVT_DISCONNECTED: + LOG_INF("Peer disconnected"); + conn_handle = BLE_CONN_HANDLE_INVALID; + break; + + case BLE_GAP_EVT_AUTH_STATUS: + LOG_INF("Authentication status: %#x", + evt->evt.gap_evt.params.auth_status.auth_status); + break; + + case BLE_GAP_EVT_SEC_PARAMS_REQUEST: + LOG_INF("Pairing request"); + ble_gap_sec_params_t sec_param = { + .io_caps = BLE_GAP_IO_CAPS_NONE, + .min_key_size = 7, + .max_key_size = 16, + .kdist_own = { .id = 1, .enc = 1, }, + .kdist_peer = { .id = 1, .enc = 1, } + }; + + nrf_err = sd_ble_gap_sec_params_reply(evt->evt.gap_evt.conn_handle, + BLE_GAP_SEC_STATUS_SUCCESS, &sec_param, NULL); + if (nrf_err) { + LOG_ERR("Failed to reply to security request, nrf_error %d", nrf_err); + } + break; + + case BLE_GATTS_EVT_SYS_ATTR_MISSING: + LOG_INF("BLE_GATTS_EVT_SYS_ATTR_MISSING"); + /* No system attributes have been stored */ + nrf_err = sd_ble_gatts_sys_attr_set(conn_handle, NULL, 0, 0); + if (nrf_err) { + LOG_ERR("Failed to set system attributes, nrf_error %d", nrf_err); + } + break; + } +} +NRF_SDH_BLE_OBSERVER(sdh_ble, on_ble_evt, NULL, 0); + +static void ble_adv_evt_handler(struct ble_adv *ble_adv, const struct ble_adv_evt *evt) +{ + switch (evt->evt_type) { + case BLE_ADV_EVT_ERROR: + LOG_ERR("Advertising error %d", evt->error.reason); + break; + default: + break; + } +} + +static void on_hids_evt(struct ble_hids *hids, const struct ble_hids_evt *hids_evt) +{ + switch (hids_evt->evt_type) { + case BLE_HIDS_EVT_HOST_SUSP: + LOG_INF("BLE_HIDS_EVT_HOST_SUSP"); + break; + case BLE_HIDS_EVT_HOST_EXIT_SUSP: + LOG_INF("BLE_HIDS_EVT_HOST_EXIT_SUSP"); + break; + case BLE_HIDS_EVT_NOTIF_ENABLED: + LOG_INF("BLE_HIDS_EVT_NOTIF_ENABLED"); + break; + case BLE_HIDS_EVT_NOTIF_DISABLED: + LOG_INF("BLE_HIDS_EVT_NOTIF_DISABLED"); + break; + case BLE_HIDS_EVT_REP_CHAR_WRITE: + LOG_INF("BLE_HIDS_EVT_REP_CHAR_WRITE"); + break; + case BLE_HIDS_EVT_BOOT_MODE_ENTERED: + LOG_INF("BLE_HIDS_EVT_BOOT_MODE_ENTERED"); + break; + case BLE_HIDS_EVT_REPORT_MODE_ENTERED: + LOG_INF("BLE_HIDS_EVT_REPORT_MODE_ENTERED"); + break; + case BLE_HIDS_EVT_REPORT_READ: + LOG_INF("BLE_HIDS_EVT_REPORT_READ"); + break; + default: + break; + } +} + +static uint32_t hids_init(void) +{ + struct ble_hids_report_config *p_input_report; + struct ble_hids_report_config *p_output_report; + struct ble_hids_report_config *p_feature_report; + + static struct ble_hids_report_config input_report_array[1]; + static struct ble_hids_report_config output_report_array[1]; + static struct ble_hids_report_config feature_report_array[1]; + static uint8_t report_map_data[] = { + 0x05, 0x01, /* Usage Page (Generic Desktop) */ + 0x09, 0x06, /* Usage (Keyboard) */ + 0xA1, 0x01, /* Collection (Application) */ + 0x05, 0x07, /* Usage Page (Key Codes) */ + 0x19, 0xe0, /* Usage Minimum (224) */ + 0x29, 0xe7, /* Usage Maximum (231) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x25, 0x01, /* Logical Maximum (1) */ + 0x75, 0x01, /* Report Size (1) */ + 0x95, 0x08, /* Report Count (8) */ + 0x81, 0x02, /* Input (Data, Variable, Absolute) */ + + 0x95, 0x01, /* Report Count (1) */ + 0x75, 0x08, /* Report Size (8) */ + 0x81, 0x01, /* Input (Constant) reserved byte(1) */ + + 0x95, 0x05, /* Report Count (5) */ + 0x75, 0x01, /* Report Size (1) */ + 0x05, 0x08, /* Usage Page (Page# for LEDs) */ + 0x19, 0x01, /* Usage Minimum (1) */ + 0x29, 0x05, /* Usage Maximum (5) */ + 0x91, 0x02, /* Output (Data, Variable, Absolute), Led report */ + 0x95, 0x01, /* Report Count (1) */ + 0x75, 0x03, /* Report Size (3) */ + 0x91, 0x01, /* Output (Data, Variable, Absolute), Led report padding */ + + 0x95, 0x06, /* Report Count (6) */ + 0x75, 0x08, /* Report Size (8) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x25, 0x65, /* Logical Maximum (101) */ + 0x05, 0x07, /* Usage Page (Key codes) */ + 0x19, 0x00, /* Usage Minimum (0) */ + 0x29, 0x65, /* Usage Maximum (101) */ + 0x81, 0x00, /* Input (Data, Array) Key array(6 bytes) */ + + 0x09, 0x05, /* Usage (Vendor Defined) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x26, 0xFF, 0x00, /* Logical Maximum (255) */ + 0x75, 0x08, /* Report Size (8 bit) */ + 0x95, 0x02, /* Report Count (2) */ + 0xB1, 0x02, /* Feature (Data, Variable, Absolute) */ + + 0xC0 /* End Collection (Application) */ + }; + + memset((void *)input_report_array, 0, sizeof(input_report_array)); + memset((void *)output_report_array, 0, sizeof(output_report_array)); + memset((void *)feature_report_array, 0, sizeof(feature_report_array)); + + /* Initialize HID Service */ + p_input_report = &input_report_array[INPUT_REPORT_KEYS_INDEX]; + p_input_report->len = INPUT_REPORT_KEYS_MAX_LEN; + p_input_report->report_id = INPUT_REP_REF_ID; + p_input_report->report_type = BLE_HIDS_REPORT_TYPE_INPUT; + + p_input_report->sec.read = BLE_GAP_CONN_SEC_MODE_OPEN; + p_input_report->sec.write = BLE_GAP_CONN_SEC_MODE_OPEN; + p_input_report->sec.cccd_write = BLE_GAP_CONN_SEC_MODE_OPEN; + + p_output_report = &output_report_array[OUTPUT_REPORT_INDEX]; + p_output_report->len = OUTPUT_REPORT_MAX_LEN; + p_output_report->report_id = OUTPUT_REP_REF_ID; + p_output_report->report_type = BLE_HIDS_REPORT_TYPE_OUTPUT; + + p_output_report->sec.read = BLE_GAP_CONN_SEC_MODE_OPEN; + p_output_report->sec.write = BLE_GAP_CONN_SEC_MODE_OPEN; + p_output_report->sec.cccd_write = BLE_GAP_CONN_SEC_MODE_NO_ACCESS; + + p_feature_report = &feature_report_array[FEATURE_REPORT_INDEX]; + p_feature_report->len = FEATURE_REPORT_MAX_LEN; + p_feature_report->report_id = FEATURE_REP_REF_ID; + p_feature_report->report_type = BLE_HIDS_REPORT_TYPE_FEATURE; + + p_feature_report->sec.read = BLE_GAP_CONN_SEC_MODE_OPEN; + p_feature_report->sec.write = BLE_GAP_CONN_SEC_MODE_OPEN; + + struct ble_hids_config hids_config = { + .evt_handler = on_hids_evt, + .input_report = input_report_array, + .output_report = output_report_array, + .feature_report = feature_report_array, + .input_report_count = 1, + .output_report_count = 1, + .feature_report_count = 1, + .hid_information = { + .bcd_hid = BASE_USB_HID_SPEC_VERSION, + .b_country_code = 0, + .bcd_flags = { + .normally_connectable = 1, + .remote_wake = 1, + }, + .sec.read = BLE_GAP_CONN_SEC_MODE_OPEN, + }, + .report_map = { + .data = report_map_data, + .len = sizeof(report_map_data), + .sec.read = BLE_GAP_CONN_SEC_MODE_OPEN, + }, + .included_services_count = 0, + .included_services_array = NULL, + }; + + return ble_hids_init(&ble_hids, &hids_config); +} + +static uint8_t button_ctrl_code_get(uint8_t key) +{ + if (INPUT_REPORT_KEYS_CTRL_CODE_MIN <= key && key <= INPUT_REPORT_KEYS_CTRL_CODE_MAX) { + return (uint8_t)(1U << (key - INPUT_REPORT_KEYS_CTRL_CODE_MIN)); + } + return 0; +} + + +static int key_set(struct ble_hids *hids, uint8_t *report, size_t report_size, uint8_t key) +{ + uint8_t ctrl_mask = button_ctrl_code_get(key); + + if (ctrl_mask) { + report[0] |= ctrl_mask; + return 0; + } + for (size_t i = 0; i < (report_size - 2); ++i) { + if (report[i + 2] == 0) { + report[i + 2] = key; + return 0; + } + } + /* All slots busy */ + return -EBUSY; +} + + +static int key_clear(struct ble_hids *hids, uint8_t *report, size_t report_size, uint8_t key) +{ + uint8_t ctrl_mask = button_ctrl_code_get(key); + + if (ctrl_mask) { + report[0] &= ~ctrl_mask; + return 0; + } + for (size_t i = 0; i < (report_size - 2); ++i) { + if (report[i + 2] == key) { + report[i + 2] = 0; + return 0; + } + } + + /* Key not found */ + return -EINVAL; +} + +static int on_key_press(struct ble_hids *hids, const char key, bool pressed) +{ + uint32_t nrf_err; + + if (pressed) { + key_set(hids, keys_report, sizeof(keys_report), key); + } else { + key_clear(hids, keys_report, sizeof(keys_report), key); + } + + nrf_err = ble_hids_inp_rep_send(hids, conn_handle, INPUT_REPORT_KEYS_INDEX, keys_report, + sizeof(keys_report)); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to send input report, nrf_error %#x", nrf_err); + } + + return 0; +} + +static void button_handler(uint8_t pin, uint8_t action) +{ + static const char hello_world_str[] = { + 0x0b, /* Key h */ + 0x08, /* Key e */ + 0x0f, /* Key l */ + 0x0f, /* Key l */ + 0x12, /* Key o */ + 0x28, /* Key Return */ + }; + static const char *chr = hello_world_str; + + if (conn_handle == BLE_CONN_HANDLE_INVALID) { + return; + } + + switch (pin) { + case BOARD_PIN_BTN_0: + if (action == BTN_PRESSED) { + on_key_press(&ble_hids, *chr, true); + } else { + on_key_press(&ble_hids, *chr, false); + if (++chr == (hello_world_str + sizeof(hello_world_str))) { + chr = hello_world_str; + } + } + break; + case BOARD_PIN_BTN_1: + on_key_press(&ble_hids, 0xE1, action == BTN_PRESSED); /* Shift */ + break; + case BOARD_PIN_BTN_2: + case BOARD_PIN_BTN_3: + /* Reserved for pairing */ + break; + } +} + +int main(void) +{ + int err; + uint32_t nrf_err; + + struct ble_adv_config ble_adv_cfg = { + .conn_cfg_tag = CONFIG_NRF_SDH_BLE_CONN_TAG, + .evt_handler = ble_adv_evt_handler, + .adv_data = { + .name_type = BLE_ADV_DATA_FULL_NAME, + .flags = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE, + }, + }; + + struct bm_buttons_config configs[4] = { + { + .pin_number = BOARD_PIN_BTN_0, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + { + .pin_number = BOARD_PIN_BTN_1, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + { + .pin_number = BOARD_PIN_BTN_2, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + { + .pin_number = BOARD_PIN_BTN_3, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + }; + + nrf_gpio_cfg_output(BOARD_PIN_LED_0); + + err = bm_buttons_init(configs, ARRAY_SIZE(configs), BM_BUTTONS_DETECTION_DELAY_MIN_US); + if (err) { + printk("Failed to initialize buttons, err %d\n", err); + return err; + } + + err = bm_buttons_enable(); + if (err) { + printk("Failed to enable buttons, err %d\n", err); + return err; + } + + err = nrf_sdh_enable_request(); + if (err) { + printk("Failed to enable SoftDevice, err %d\n", err); + return -1; + } + + printk("SoftDevice enabled\n"); + + err = nrf_sdh_ble_enable(CONFIG_NRF_SDH_BLE_CONN_TAG); + if (err) { + printk("Failed to enable BLE, err %d\n", err); + return -1; + } + + printk("Bluetooth is enabled!\n"); + + err = ble_dis_init(); + if (err) { + printk("Failed to initialize device information service, err %d\n", err); + return -1; + } + + nrf_err = hids_init(); + if (nrf_err != NRF_SUCCESS) { + printk("Failed to initialize HIDS, nrf_error %#x", err); + return -1; + } + + printk("HIDS initialized\n"); + + err = ble_adv_init(&ble_adv, &ble_adv_cfg); + if (err) { + printk("Failed to initialize BLE advertising, err %d\n", err); + return -1; + } + + err = ble_adv_start(&ble_adv, BLE_ADV_MODE_FAST); + if (err) { + printk("Failed to start advertising, err %d\n", err); + return -1; + } + + while (true) { + while (LOG_PROCESS()) { + /* Empty. */ + } + sd_app_evt_wait(); + } + + return 0; +} diff --git a/samples/bluetooth/ble_hids_mouse/CMakeLists.txt b/samples/bluetooth/ble_hids_mouse/CMakeLists.txt new file mode 100644 index 0000000000..32533246e1 --- /dev/null +++ b/samples/bluetooth/ble_hids_mouse/CMakeLists.txt @@ -0,0 +1,12 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(ble_hids_mouse) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/bluetooth/ble_hids_mouse/Kconfig b/samples/bluetooth/ble_hids_mouse/Kconfig new file mode 100644 index 0000000000..06407d2b0d --- /dev/null +++ b/samples/bluetooth/ble_hids_mouse/Kconfig @@ -0,0 +1,16 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menu "BLE HIDS Mouse sample" + +module=BLE_HIDS_MOUSE_SAMPLE +module-dep=LOG +module-str=BLE HIDS Mouse Sample +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +endmenu # "BLE HIDS Mouse sample" + +source "Kconfig.zephyr" diff --git a/samples/bluetooth/ble_hids_mouse/README.rst b/samples/bluetooth/ble_hids_mouse/README.rst new file mode 100644 index 0000000000..ef6b22f885 --- /dev/null +++ b/samples/bluetooth/ble_hids_mouse/README.rst @@ -0,0 +1,69 @@ +.. _ble_hids_mouse_sample: + +Bluetooth: Human Interface Device Service Mouse +############################################### + +.. contents:: + :local: + :depth: 2 + +The Peripheral HIDS mouse sample demonstrates how to use the :ref:`hids_readme` to implement a mouse input device that you can connect to your computer. + +Requirements +************ + +The sample supports the following development kits: + +.. list-table:: + :header-rows: 1 + + * - Hardware platform + - PCA + - SoftDevice + - Board target + * - `nRF54L15 DK`_ + - PCA10156 + - S115 + - bm_nrf54l15dk/nrf54l15/cpuapp/softdevice_s115 + * - `nRF54L15 DK`_ (emulating nRF54L10) + - PCA10156 + - S115 + - bm_nrf54l15dk/nrf54l10/cpuapp/softdevice_s115 + * - `nRF54L15 DK`_ (emulating nRF54L05) + - PCA10156 + - S115 + - bm_nrf54l15dk/nrf54l05/cpuapp/softdevice_s115 + +Overview +******** + +The sample uses the buttons on a development kit to simulate the movement of a mouse. +The four buttons simulate movement to the left, up, right, and down, respectively. +Mouse clicks are not simulated. + +This sample exposes the HID GATT Service. +It uses a report map for a generic mouse. + +Programming the S115 SoftDevice +******************************* + +.. include:: /includes/softdevice_flash.txt + +.. _ble_hids_mouse_sample_testing: + + +Building and running +******************** + +This sample can be found under :file:`samples/bluetooth/ble_hids_mouse/` in the |BMshort| folder structure. + +.. include:: /includes/create_sample.txt + +.. include:: /includes/configure_and_build_sample.txt + +.. include:: /includes/program_sample.txt + +Testing +======= + +TODO diff --git a/samples/bluetooth/ble_hids_mouse/prj.conf b/samples/bluetooth/ble_hids_mouse/prj.conf new file mode 100644 index 0000000000..eb981f5042 --- /dev/null +++ b/samples/bluetooth/ble_hids_mouse/prj.conf @@ -0,0 +1,37 @@ +CONFIG_LOG=y +CONFIG_LOG_BACKEND_BM_UARTE=y + +CONFIG_BLE_ADV=y +CONFIG_BLE_ADV_NAME="nRF_BM_HIDS_MOUSE" +CONFIG_BLE_ADV_EXTENDED_ADVERTISING=n +CONFIG_BLE_ADV_DIRECTED_ADVERTISING=n + +CONFIG_SOFTDEVICE=y +CONFIG_NRF_SDH=y + +# Enable RNG +CONFIG_NRF_SECURITY=y +CONFIG_MBEDTLS_PSA_CRYPTO_C=y +CONFIG_PSA_WANT_GENERATE_RANDOM=y + +# BLE connection parameter +CONFIG_BLE_CONN_PARAMS=y + +# Device information service +CONFIG_BLE_DIS=y +CONFIG_BLE_DIS_SERIAL_NUMBER="ABCD" +CONFIG_BLE_DIS_HW_REVISION="hw 54.15.0" +CONFIG_BLE_DIS_FW_REVISION="fw 17.2.0" +CONFIG_BLE_DIS_SW_REVISION="sw 1.0.0" + +# Battery service +CONFIG_BLE_BAS=y + +# Buttons and timer +CONFIG_BM_TIMER=y +CONFIG_BM_BUTTONS=y + +# HIDS service +CONFIG_BLE_HIDS=y +CONFIG_BLE_HIDS_BOOT_MOUSE=y +CONFIG_BLE_HIDS_LOG_LEVEL_DBG=y diff --git a/samples/bluetooth/ble_hids_mouse/sample.yaml b/samples/bluetooth/ble_hids_mouse/sample.yaml new file mode 100644 index 0000000000..26f0014118 --- /dev/null +++ b/samples/bluetooth/ble_hids_mouse/sample.yaml @@ -0,0 +1,15 @@ +sample: + name: BLE HIDS Mouse +tests: + sample.ble_hids_mouse: + sysbuild: true + build_only: true + integration_platforms: + - bm_nrf54l15dk/nrf54l05/cpuapp/s115_softdevice + - bm_nrf54l15dk/nrf54l10/cpuapp/s115_softdevice + - bm_nrf54l15dk/nrf54l15/cpuapp/s115_softdevice + platform_allow: + - bm_nrf54l15dk/nrf54l05/cpuapp/s115_softdevice + - bm_nrf54l15dk/nrf54l10/cpuapp/s115_softdevice + - bm_nrf54l15dk/nrf54l15/cpuapp/s115_softdevice + tags: ci_build diff --git a/samples/bluetooth/ble_hids_mouse/src/main.c b/samples/bluetooth/ble_hids_mouse/src/main.c new file mode 100644 index 0000000000..e2207e9ed1 --- /dev/null +++ b/samples/bluetooth/ble_hids_mouse/src/main.c @@ -0,0 +1,462 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +LOG_MODULE_REGISTER(app, CONFIG_BLE_HIDS_MOUSE_SAMPLE_LOG_LEVEL); + +#define BASE_USB_HID_SPEC_VERSION 0x0101 + +/**< Number of pixels by which the cursor is moved each time a button is pushed. */ +#define MOVEMENT_SPEED 5 +/**< Number of input reports in this application. */ +#define INPUT_REPORT_COUNT 3 +/**< Length of Mouse Input Report containing button data. */ +#define INPUT_REP_BUTTONS_LEN 3 +/**< Length of Mouse Input Report containing movement data. */ +#define INPUT_REP_MOVEMENT_LEN 3 +/**< Length of Mouse Input Report containing media player data. */ +#define INPUT_REP_MEDIA_PLAYER_LEN 1 +/**< Index of Mouse Input Report containing button data. */ +#define INPUT_REP_BUTTONS_INDEX 0 +/**< Index of Mouse Input Report containing movement data. */ +#define INPUT_REP_MOVEMENT_INDEX 1 +/**< Index of Mouse Input Report containing media player data. */ +#define INPUT_REP_MPLAYER_INDEX 2 +/**< Id of reference to Mouse Input Report containing button data. */ +#define INPUT_REP_REF_BUTTONS_ID 1 +/**< Id of reference to Mouse Input Report containing movement data. */ +#define INPUT_REP_REF_MOVEMENT_ID 2 +/**< Id of reference to Mouse Input Report containing media player data. */ +#define INPUT_REP_REF_MPLAYER_ID 3 + +#define BTN_PRESSED 1 + +/* HID service instance */ +BLE_HIDS_DEF(ble_hids); +/* BLE Advertising library instance */ +BLE_ADV_DEF(ble_adv); + +/* BLE Connection handle */ +static uint16_t conn_handle = BLE_CONN_HANDLE_INVALID; + +static bool boot_mode; + +static void on_ble_evt(const ble_evt_t *evt, void *ctx) +{ + int err; + + switch (evt->header.evt_id) { + case BLE_GAP_EVT_CONNECTED: + LOG_INF("Peer connected"); + conn_handle = evt->evt.gap_evt.conn_handle; + err = sd_ble_gatts_sys_attr_set(conn_handle, NULL, 0, 0); + if (err) { + LOG_ERR("Failed to set system attributes, nrf_error %d", err); + } + break; + case BLE_GAP_EVT_DISCONNECTED: + LOG_INF("Peer disconnected"); + conn_handle = BLE_CONN_HANDLE_INVALID; + break; + + case BLE_GAP_EVT_AUTH_STATUS: + LOG_INF("Authentication status: %#x", + evt->evt.gap_evt.params.auth_status.auth_status); + break; + + case BLE_GAP_EVT_SEC_PARAMS_REQUEST: + LOG_INF("Pairing request"); + ble_gap_sec_params_t sec_param = { + .io_caps = BLE_GAP_IO_CAPS_NONE, + .min_key_size = 7, + .max_key_size = 16, + .kdist_own = { .id = 1, .enc = 1, }, + .kdist_peer = { .id = 1, .enc = 1, } + }; + + err = sd_ble_gap_sec_params_reply(evt->evt.gap_evt.conn_handle, + BLE_GAP_SEC_STATUS_SUCCESS, &sec_param, NULL); + if (err) { + LOG_ERR("Failed to reply to security request, nrf_error %d", err); + } + break; + + case BLE_GATTS_EVT_SYS_ATTR_MISSING: + LOG_INF("BLE_GATTS_EVT_SYS_ATTR_MISSING"); + /* No system attributes have been stored */ + err = sd_ble_gatts_sys_attr_set(conn_handle, NULL, 0, 0); + if (err) { + LOG_ERR("Failed to set system attributes, nrf_error %d", err); + } + break; + } +} +NRF_SDH_BLE_OBSERVER(sdh_ble, on_ble_evt, NULL, 0); + +static void ble_adv_evt_handler(struct ble_adv *ble_adv, const struct ble_adv_evt *evt) +{ + switch (evt->evt_type) { + case BLE_ADV_EVT_ERROR: + LOG_ERR("Advertising error %d", evt->error.reason); + break; + default: + break; + } +} + +static void on_hids_evt(struct ble_hids *hids, const struct ble_hids_evt *hids_evt) +{ + switch (hids_evt->evt_type) { + case BLE_HIDS_EVT_HOST_SUSP: + LOG_INF("BLE_HIDS_EVT_HOST_SUSP"); + break; + case BLE_HIDS_EVT_HOST_EXIT_SUSP: + LOG_INF("BLE_HIDS_EVT_HOST_EXIT_SUSP"); + break; + case BLE_HIDS_EVT_NOTIF_ENABLED: + LOG_INF("BLE_HIDS_EVT_NOTIF_ENABLED"); + break; + case BLE_HIDS_EVT_NOTIF_DISABLED: + LOG_INF("BLE_HIDS_EVT_NOTIF_DISABLED"); + break; + case BLE_HIDS_EVT_REP_CHAR_WRITE: + LOG_INF("BLE_HIDS_EVT_REP_CHAR_WRITE"); + break; + case BLE_HIDS_EVT_BOOT_MODE_ENTERED: + LOG_INF("BLE_HIDS_EVT_BOOT_MODE_ENTERED"); + boot_mode = true; + break; + case BLE_HIDS_EVT_REPORT_MODE_ENTERED: + LOG_INF("BLE_HIDS_EVT_REPORT_MODE_ENTERED"); + boot_mode = false; + break; + case BLE_HIDS_EVT_REPORT_READ: + LOG_INF("BLE_HIDS_EVT_REPORT_READ"); + break; + default: + break; + } +} + +static uint32_t hids_init(void) +{ + struct ble_hids_report_config *input_report; + + static struct ble_hids_report_config input_report_array[3]; + static uint8_t report_map_data[] = { + 0x05, 0x01, /* Usage Page (Generic Desktop) */ + 0x09, 0x02, /* Usage (Mouse) */ + + 0xA1, 0x01, /* Collection (Application) */ + + /* Report ID 1: Mouse buttons + scroll/pan */ + 0x85, 0x01, /* Report Id 1 */ + 0x09, 0x01, /* Usage (Pointer) */ + 0xA1, 0x00, /* Collection (Physical) */ + 0x95, 0x05, /* Report Count (3) */ + 0x75, 0x01, /* Report Size (1) */ + 0x05, 0x09, /* Usage Page (Buttons) */ + 0x19, 0x01, /* Usage Minimum (01) */ + 0x29, 0x05, /* Usage Maximum (05) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x25, 0x01, /* Logical Maximum (1) */ + 0x81, 0x02, /* Input (Data, Variable, Absolute) */ + 0x95, 0x01, /* Report Count (1) */ + 0x75, 0x03, /* Report Size (3) */ + 0x81, 0x01, /* Input (Constant) for padding */ + 0x75, 0x08, /* Report Size (8) */ + 0x95, 0x01, /* Report Count (1) */ + 0x05, 0x01, /* Usage Page (Generic Desktop) */ + 0x09, 0x38, /* Usage (Wheel) */ + 0x15, 0x81, /* Logical Minimum (-127) */ + 0x25, 0x7F, /* Logical Maximum (127) */ + 0x81, 0x06, /* Input (Data, Variable, Relative) */ + 0x05, 0x0C, /* Usage Page (Consumer) */ + 0x0A, 0x38, 0x02, /* Usage (AC Pan) */ + 0x95, 0x01, /* Report Count (1) */ + 0x81, 0x06, /* Input (Data,Value,Relative,Bit Field) */ + 0xC0, /* End Collection (Physical) */ + + /* Report ID 2: Mouse motion */ + 0x85, 0x02, /* Report Id 2 */ + 0x09, 0x01, /* Usage (Pointer) */ + 0xA1, 0x00, /* Collection (Physical) */ + 0x75, 0x0C, /* Report Size (12) */ + 0x95, 0x02, /* Report Count (2) */ + 0x05, 0x01, /* Usage Page (Generic Desktop) */ + 0x09, 0x30, /* Usage (X) */ + 0x09, 0x31, /* Usage (Y) */ + 0x16, 0x01, 0xF8, /* Logical maximum (2047) */ + 0x26, 0xFF, 0x07, /* Logical minimum (-2047) */ + 0x81, 0x06, /* Input (Data, Variable, Relative) */ + 0xC0, /* End Collection (Physical) */ + 0xC0, /* End Collection (Application) */ + + /* Report ID 3: Advanced buttons */ + 0x05, 0x0C, /* Usage Page (Consumer) */ + 0x09, 0x01, /* Usage (Consumer Control) */ + 0xA1, 0x01, /* Collection (Application) */ + 0x85, 0x03, /* Report Id (3) */ + 0x15, 0x00, /* Logical minimum (0) */ + 0x25, 0x01, /* Logical maximum (1) */ + 0x75, 0x01, /* Report Size (1) */ + 0x95, 0x01, /* Report Count (1) */ + + 0x09, 0xCD, /* Usage (Play/Pause) */ + 0x81, 0x06, /* Input (Data,Value,Relative,Bit Field) */ + 0x0A, 0x83, 0x01, /* Usage (Consumer Control Configuration) */ + 0x81, 0x06, /* Input (Data,Value,Relative,Bit Field) */ + 0x09, 0xB5, /* Usage (Scan Next Track) */ + 0x81, 0x06, /* Input (Data,Value,Relative,Bit Field) */ + 0x09, 0xB6, /* Usage (Scan Previous Track) */ + 0x81, 0x06, /* Input (Data,Value,Relative,Bit Field) */ + + 0x09, 0xEA, /* Usage (Volume Down) */ + 0x81, 0x06, /* Input (Data,Value,Relative,Bit Field) */ + 0x09, 0xE9, /* Usage (Volume Up) */ + 0x81, 0x06, /* Input (Data,Value,Relative,Bit Field) */ + 0x0A, 0x25, 0x02, /* Usage (AC Forward) */ + 0x81, 0x06, /* Input (Data,Value,Relative,Bit Field) */ + 0x0A, 0x24, 0x02, /* Usage (AC Back) */ + 0x81, 0x06, /* Input (Data,Value,Relative,Bit Field) */ + 0xC0 /* End Collection */ + }; + + memset((void *)input_report_array, 0, sizeof(input_report_array)); + + /* Initialize HID Service */ + input_report = &input_report_array[INPUT_REP_BUTTONS_INDEX]; + input_report->len = INPUT_REP_BUTTONS_LEN; + input_report->report_id = INPUT_REP_REF_BUTTONS_ID; + input_report->report_type = BLE_HIDS_REPORT_TYPE_INPUT; + + input_report->sec.read = BLE_GAP_CONN_SEC_MODE_OPEN; + input_report->sec.write = BLE_GAP_CONN_SEC_MODE_OPEN; + input_report->sec.cccd_write = BLE_GAP_CONN_SEC_MODE_OPEN; + + input_report = &input_report_array[INPUT_REP_MOVEMENT_INDEX]; + input_report->len = INPUT_REP_MOVEMENT_LEN; + input_report->report_id = INPUT_REP_REF_MOVEMENT_ID; + input_report->report_type = BLE_HIDS_REPORT_TYPE_INPUT; + + input_report->sec.read = BLE_GAP_CONN_SEC_MODE_OPEN; + input_report->sec.write = BLE_GAP_CONN_SEC_MODE_OPEN; + input_report->sec.cccd_write = BLE_GAP_CONN_SEC_MODE_OPEN; + + input_report = &input_report_array[INPUT_REP_MPLAYER_INDEX]; + input_report->len = INPUT_REP_MEDIA_PLAYER_LEN; + input_report->report_id = INPUT_REP_REF_MPLAYER_ID; + input_report->report_type = BLE_HIDS_REPORT_TYPE_INPUT; + + input_report->sec.read = BLE_GAP_CONN_SEC_MODE_OPEN; + input_report->sec.write = BLE_GAP_CONN_SEC_MODE_OPEN; + input_report->sec.cccd_write = BLE_GAP_CONN_SEC_MODE_OPEN; + + struct ble_hids_config hids_config = { + .evt_handler = on_hids_evt, + .input_report = input_report_array, + .output_report = NULL, + .feature_report = NULL, + .input_report_count = ARRAY_SIZE(input_report_array), + .output_report_count = 0, + .feature_report_count = 0, + .hid_information = { + .bcd_hid = BASE_USB_HID_SPEC_VERSION, + .b_country_code = 0, + .bcd_flags = { + .normally_connectable = 1, + .remote_wake = 1, + }, + .sec.read = BLE_GAP_CONN_SEC_MODE_OPEN, + }, + .report_map = { + .data = report_map_data, + .len = sizeof(report_map_data), + .sec.read = BLE_GAP_CONN_SEC_MODE_OPEN, + }, + .included_services_count = 0, + .included_services_array = NULL, + }; + + return ble_hids_init(&ble_hids, &hids_config); +} + +static void mouse_movement_send(struct ble_hids *hids, int16_t delta_x, int16_t delta_y) +{ + uint32_t nrf_err; + struct ble_hids_boot_mouse_input_report inp_rep = { + .buttons = 0x00, + }; + + if (boot_mode) { + inp_rep.delta_x = MIN(delta_x, 0x00ff); + inp_rep.delta_y = MIN(delta_y, 0x00ff); + + nrf_err = ble_hids_boot_mouse_inp_rep_send(hids, conn_handle, &inp_rep); + } else { + uint8_t buffer[INPUT_REP_MOVEMENT_LEN]; + + delta_x = MIN(delta_x, 0x00ff); + delta_y = MIN(delta_y, 0x0fff); + + buffer[0] = delta_x & 0x00ff; + buffer[1] = ((delta_y & 0x000f) << 4) | ((delta_x & 0x0f00) >> 8); + buffer[2] = (delta_y & 0x0ff0) >> 4; + + nrf_err = ble_hids_inp_rep_send(hids, conn_handle, INPUT_REP_MOVEMENT_INDEX, + buffer, INPUT_REP_MOVEMENT_LEN); + } + + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to send input report, nrf_error %#x", nrf_err); + } +} + +static void button_handler(uint8_t pin, uint8_t action) +{ + if (conn_handle == BLE_CONN_HANDLE_INVALID || action != BTN_PRESSED) { + return; + } + + switch (pin) { + case BOARD_PIN_BTN_0: + mouse_movement_send(&ble_hids, -MOVEMENT_SPEED, 0); + break; + case BOARD_PIN_BTN_1: + mouse_movement_send(&ble_hids, 0, -MOVEMENT_SPEED); + break; + case BOARD_PIN_BTN_2: + mouse_movement_send(&ble_hids, MOVEMENT_SPEED, 0); + break; + case BOARD_PIN_BTN_3: + mouse_movement_send(&ble_hids, 0, MOVEMENT_SPEED); + break; + } +} + +int main(void) +{ + int err; + uint32_t nrf_err; + + struct ble_adv_config ble_adv_cfg = { + .conn_cfg_tag = CONFIG_NRF_SDH_BLE_CONN_TAG, + .evt_handler = ble_adv_evt_handler, + .adv_data = { + .name_type = BLE_ADV_DATA_FULL_NAME, + .flags = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE, + }, + }; + + struct bm_buttons_config configs[4] = { + { + .pin_number = BOARD_PIN_BTN_0, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + { + .pin_number = BOARD_PIN_BTN_1, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + { + .pin_number = BOARD_PIN_BTN_2, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + { + .pin_number = BOARD_PIN_BTN_3, + .active_state = BM_BUTTONS_ACTIVE_LOW, + .pull_config = BM_BUTTONS_PIN_PULLUP, + .handler = button_handler, + }, + }; + + err = bm_buttons_init(configs, ARRAY_SIZE(configs), BM_BUTTONS_DETECTION_DELAY_MIN_US); + if (err) { + printk("Failed to initialize buttons, err: %d\n", err); + return err; + } + + err = bm_buttons_enable(); + if (err) { + printk("Failed to enable buttons, err: %d\n", err); + return err; + } + + err = nrf_sdh_enable_request(); + if (err) { + printk("Failed to enable SoftDevice, err %d\n", err); + return -1; + } + + printk("SoftDevice enabled\n"); + + err = nrf_sdh_ble_enable(CONFIG_NRF_SDH_BLE_CONN_TAG); + if (err) { + printk("Failed to enable BLE, err %d\n", err); + return -1; + } + + printk("Bluetooth is enabled!\n"); + + err = ble_dis_init(); + if (err) { + printk("Failed to initialize device information service, err %d\n", err); + return -1; + } + + nrf_err = hids_init(); + if (nrf_err != NRF_SUCCESS) { + printk("Failed to initialize HIDS, nrf_error %#x", nrf_err); + return -1; + } + + printk("HIDS initialized\n"); + + err = ble_adv_init(&ble_adv, &ble_adv_cfg); + if (err) { + printk("Failed to initialize BLE advertising, err %d\n", err); + return -1; + } + + err = ble_adv_start(&ble_adv, BLE_ADV_MODE_FAST); + if (err) { + printk("Failed to start advertising, err %d\n", err); + return -1; + } + + while (true) { + while (LOG_PROCESS()) { + /* Empty. */ + } + sd_app_evt_wait(); + } + + return 0; +} diff --git a/subsys/bluetooth/services/CMakeLists.txt b/subsys/bluetooth/services/CMakeLists.txt index b3176b0444..fed1f48964 100644 --- a/subsys/bluetooth/services/CMakeLists.txt +++ b/subsys/bluetooth/services/CMakeLists.txt @@ -6,6 +6,7 @@ add_subdirectory_ifdef(CONFIG_BLE_BAS ble_bas) add_subdirectory_ifdef(CONFIG_BLE_CGMS ble_cgms) add_subdirectory_ifdef(CONFIG_BLE_DIS ble_dis) +add_subdirectory_ifdef(CONFIG_BLE_HIDS ble_hids) add_subdirectory_ifdef(CONFIG_BLE_HRS ble_hrs) add_subdirectory_ifdef(CONFIG_BLE_LBS ble_lbs) add_subdirectory_ifdef(CONFIG_BLE_NUS ble_nus) diff --git a/subsys/bluetooth/services/Kconfig b/subsys/bluetooth/services/Kconfig index c43c7e79e0..fcc06c2773 100644 --- a/subsys/bluetooth/services/Kconfig +++ b/subsys/bluetooth/services/Kconfig @@ -6,6 +6,7 @@ rsource "ble_bas/Kconfig" rsource "ble_cgms/Kconfig" rsource "ble_dis/Kconfig" +rsource "ble_hids/Kconfig" rsource "ble_hrs/Kconfig" rsource "ble_lbs/Kconfig" rsource "ble_nus/Kconfig" diff --git a/subsys/bluetooth/services/ble_hids/CMakeLists.txt b/subsys/bluetooth/services/ble_hids/CMakeLists.txt new file mode 100644 index 0000000000..7c9f302b48 --- /dev/null +++ b/subsys/bluetooth/services/ble_hids/CMakeLists.txt @@ -0,0 +1,6 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +zephyr_sources(hids.c) diff --git a/subsys/bluetooth/services/ble_hids/Kconfig b/subsys/bluetooth/services/ble_hids/Kconfig new file mode 100644 index 0000000000..05f779cb96 --- /dev/null +++ b/subsys/bluetooth/services/ble_hids/Kconfig @@ -0,0 +1,151 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +menuconfig BLE_HIDS + bool "HID service" + depends on SOFTDEVICE_PERIPHERAL + +if BLE_HIDS + +config BLE_HIDS_BOOT_MOUSE + bool "Add boot mouse input characteristic" + help + Add the boot mouse input characteristic. + +menu "Boot mouse input characteristic security mode" + visible if BLE_HIDS_BOOT_MOUSE + +characteristic=BLE_HIDS_BOOT_MOUSE_INPUT_CHAR_READ +characteristic-str=read operation +rsource "${ZEPHYR_NRF_BM_MODULE_DIR}/subsys/bluetooth/services/Kconfig.template.sec_mode" + +characteristic=BLE_HIDS_BOOT_MOUSE_INPUT_CCCD_WRITE +characteristic-str=CCCD write operation +rsource "${ZEPHYR_NRF_BM_MODULE_DIR}/subsys/bluetooth/services/Kconfig.template.sec_mode" + +endmenu + +config BLE_HIDS_BOOT_KEYBOARD + bool "Add boot keyboard characteristics" + help + Add the boot keyboard input and output characteristics. + +menu "Boot keyboard input report characteristic security mode" + visible if BLE_HIDS_BOOT_KEYBOARD + +characteristic=BLE_HIDS_BOOT_KEYBOARD_INPUT_CHAR_READ +characteristic-str=read operation +rsource "${ZEPHYR_NRF_BM_MODULE_DIR}/subsys/bluetooth/services/Kconfig.template.sec_mode" + +characteristic=BLE_HIDS_BOOT_KEYBOARD_INPUT_CCCD_WRITE +characteristic-str=CCCD write operation +rsource "${ZEPHYR_NRF_BM_MODULE_DIR}/subsys/bluetooth/services/Kconfig.template.sec_mode" + +endmenu + +menu "Boot keyboard output report characteristic security mode" + visible if BLE_HIDS_BOOT_KEYBOARD + +characteristic=BLE_HIDS_BOOT_KEYBOARD_OUTPUT_CHAR_READ +characteristic-str=read operation +rsource "${ZEPHYR_NRF_BM_MODULE_DIR}/subsys/bluetooth/services/Kconfig.template.sec_mode" + +characteristic=BLE_HIDS_BOOT_KEYBOARD_OUTPUT_CHAR_WRITE +characteristic-str=write operation +rsource "${ZEPHYR_NRF_BM_MODULE_DIR}/subsys/bluetooth/services/Kconfig.template.sec_mode" + +endmenu + +choice + prompt "Default HID protocol mode" + depends on BLE_HIDS_BOOT_KEYBOARD || BLE_HIDS_BOOT_MOUSE + +config BLE_HIDS_PROTOCOL_MODE_REPORT + bool "Report" +config BLE_HIDS_PROTOCOL_MODE_BOOT + bool "Boot" + +endchoice + +config BLE_HIDS_DEFAULT_PROTOCOL_MODE + int + default 00 if BLE_HIDS_PROTOCOL_MODE_BOOT + default 01 if BLE_HIDS_PROTOCOL_MODE_REPORT + +menu "Protocol mode characteristic security mode" + +characteristic=BLE_HIDS_PROTOCOL_MODE_CHAR_READ +characteristic-str=read operation +rsource "${ZEPHYR_NRF_BM_MODULE_DIR}/subsys/bluetooth/services/Kconfig.template.sec_mode" + +characteristic=BLE_HIDS_PROTOCOL_MODE_CHAR_WRITE +characteristic-str=write operation +rsource "${ZEPHYR_NRF_BM_MODULE_DIR}/subsys/bluetooth/services/Kconfig.template.sec_mode" + +endmenu + +menu "Control point characteristic security mode" + +characteristic=BLE_HIDS_CONTROL_POINT_CHAR_WRITE +characteristic-str=write operation +rsource "${ZEPHYR_NRF_BM_MODULE_DIR}/subsys/bluetooth/services/Kconfig.template.sec_mode" + +endmenu + +menu "HID information characteristic security mode" + +characteristic=BLE_HIDS_INFORMATION_CHAR_READ +characteristic-str=read operation +rsource "${ZEPHYR_NRF_BM_MODULE_DIR}/subsys/bluetooth/services/Kconfig.template.sec_mode" + +endmenu + +config BLE_HIDS_MAX_INPUT_REP + int "Number of input reports" + range 0 8 + default 8 + help + Number of input reports + +config BLE_HIDS_INPUT_REPORT_MAX_LEN + int "Maximum input report length" if (BLE_HIDS_MAX_INPUT_REP != 0) + default 0 if (BLE_HIDS_MAX_INPUT_REP = 0) + default 8 + +config BLE_HIDS_MAX_OUTPUT_REP + int "Number of output reports" + range 0 8 + default 3 + help + Number of output reports + +config BLE_HIDS_OUTPUT_REPORT_MAX_LEN + int "Maximum output report length" if (BLE_HIDS_MAX_OUTPUT_REP != 0) + default 0 if (BLE_HIDS_MAX_OUTPUT_REP = 0) + default 1 + +config BLE_HIDS_MAX_FEATURE_REP + int "Number of feature reports" + range 0 8 + default 1 + help + Number of feature reports + +config BLE_HIDS_FEATURE_REPORT_MAX_LEN + int "Maximum feature report length" if (BLE_HIDS_MAX_FEATURE_REP != 0) + default 0 if (BLE_HIDS_MAX_FEATURE_REP = 0) + default 2 + +config BLE_HIDS_MAX_CLIENTS + int "Maximum HID clients" + range 1 NRF_SDH_BLE_PERIPHERAL_LINK_COUNT + default 1 + +module=BLE_HIDS +module-dep=LOG +module-str=BLE HID service +source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" + +endif diff --git a/subsys/bluetooth/services/ble_hids/hids.c b/subsys/bluetooth/services/ble_hids/hids.c new file mode 100644 index 0000000000..742aeab0cb --- /dev/null +++ b/subsys/bluetooth/services/ble_hids/hids.c @@ -0,0 +1,1274 @@ +/* + * Copyright (c) 2012 - 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(ble_hids, CONFIG_BLE_HIDS_LOG_LEVEL); + +/* Boot Protocol Mode. */ +#define PROTOCOL_MODE_BOOT 0x00 +/* Report Protocol Mode. */ +#define PROTOCOL_MODE_REPORT 0x01 + +/* Suspend command. */ +#define CONTROL_POINT_SUSPEND 0x00 +/* Exit Suspend command. */ +#define CONTROL_POINT_EXIT_SUSPEND 0x01 + +struct ble_hids_context { + uint8_t protocol_mode; + uint8_t control_point; +#if CONFIG_BLE_HIDS_BOOT_KEYBOARD + uint8_t boot_key_in_rep[BLE_HIDS_BOOT_KB_INPUT_REP_MAX_SIZE]; + uint8_t boot_key_out_rep[BLE_HIDS_BOOT_KB_OUTPUT_REP_MAX_SIZE]; +#endif +#if CONFIG_BLE_HIDS_BOOT_MOUSE + uint8_t boot_mouse_in_rep[BLE_HIDS_BOOT_MOUSE_INPUT_REP_MAX_SIZE]; +#endif + struct { + uint8_t data[CONFIG_BLE_HIDS_INPUT_REPORT_MAX_LEN]; + } input_reports[CONFIG_BLE_HIDS_MAX_INPUT_REP]; + struct { + uint8_t data[CONFIG_BLE_HIDS_OUTPUT_REPORT_MAX_LEN]; + } output_reports[CONFIG_BLE_HIDS_MAX_OUTPUT_REP]; + struct { + uint8_t data[CONFIG_BLE_HIDS_FEATURE_REPORT_MAX_LEN]; + } feature_reports[CONFIG_BLE_HIDS_MAX_FEATURE_REP]; +}; + +static struct ble_hids_context contexts[CONFIG_BLE_HIDS_MAX_CLIENTS]; + +static struct ble_hids_context *ble_hids_context_get(uint16_t conn_handle) +{ + return &contexts[conn_handle]; +} + +/**@brief Function for making a HID Service characteristic id. + * + * @param[in] uuid UUID of characteristic. + * @param[in] rep_type Type of report. + * @param[in] rep_index Index of the characteristic. + * + * @return HID Service characteristic id structure. + */ +static struct ble_hids_char_id make_char_id(uint16_t uuid, uint8_t rep_type, uint8_t rep_index) +{ + struct ble_hids_char_id char_id = { + .uuid = uuid, + .rep_type = rep_type, + .rep_index = rep_index, + }; + + return char_id; +} + +static void on_connected(struct ble_hids *hids, const ble_evt_t *ble_evt) +{ + uint32_t nrf_err; + struct ble_hids_context *ctx; + + if (!IS_ENABLED(CONFIG_BLE_HIDS_BOOT_KEYBOARD) && !IS_ENABLED(CONFIG_BLE_HIDS_BOOT_MOUSE)) { + return; + } + + /* Set Protocol Mode characteristic value to default value */ + ctx = ble_hids_context_get(ble_evt->evt.gap_evt.conn_handle); + ctx->protocol_mode = CONFIG_BLE_HIDS_DEFAULT_PROTOCOL_MODE; + + ble_gatts_value_t gatts_value = { + .len = sizeof(ctx->protocol_mode), + .p_value = &ctx->protocol_mode, + }; + + nrf_err = sd_ble_gatts_value_set(ble_evt->evt.gap_evt.conn_handle, + hids->protocol_mode_handles.value_handle, &gatts_value); + if (nrf_err != NRF_SUCCESS) { + LOG_WRN("Failed to set protocol mode value to default, nrf_error %#x", nrf_err); + } + + LOG_DBG("Protocol mode is %s", + IS_ENABLED(CONFIG_BLE_HIDS_PROTOCOL_MODE_BOOT) ? "boot" : "report"); +} + +static void on_disconnected(struct ble_hids *hids, const ble_evt_t *ble_evt) +{ + struct ble_hids_context *ctx; + + ctx = ble_hids_context_get(ble_evt->evt.gap_evt.conn_handle); + + /* Reset client's context */ + memset(ctx, 0, sizeof(struct ble_hids_context)); +} + +static void on_control_point_write(struct ble_hids *hids, const ble_evt_t *ble_evt) +{ + struct ble_hids_context *ctx; + struct ble_hids_evt evt = {0}; + ble_gatts_evt_write_t const *gatts_write = &ble_evt->evt.gatts_evt.params.write; + + LOG_INF("Control point value write"); + + ctx = ble_hids_context_get(ble_evt->evt.gatts_evt.conn_handle); + + switch (gatts_write->data[0]) { + case CONTROL_POINT_SUSPEND: + evt.evt_type = BLE_HIDS_EVT_HOST_SUSP; + break; + case CONTROL_POINT_EXIT_SUSPEND: + evt.evt_type = BLE_HIDS_EVT_HOST_EXIT_SUSP; + break; + default: + LOG_WRN("Unknown control point value %#x, ignoring", gatts_write->data[0]); + return; + } + + /* Store the new Control Point value for the host */ + ctx->control_point = gatts_write->data[0]; + + if (hids->evt_handler != NULL) { + evt.ble_evt = ble_evt; + hids->evt_handler(hids, &evt); + } +} + +static void on_protocol_mode_write(struct ble_hids *hids, const ble_evt_t *ble_evt) +{ + struct ble_hids_evt evt = {0}; + struct ble_hids_context *ctx; + const ble_gatts_evt_write_t *gatts_write = &ble_evt->evt.gatts_evt.params.write; + + LOG_INF("Protocol mode write"); + + ctx = ble_hids_context_get(ble_evt->evt.gatts_evt.conn_handle); + + switch (gatts_write->data[0]) { + case PROTOCOL_MODE_BOOT: + evt.evt_type = BLE_HIDS_EVT_BOOT_MODE_ENTERED; + break; + case PROTOCOL_MODE_REPORT: + evt.evt_type = BLE_HIDS_EVT_REPORT_MODE_ENTERED; + break; + default: + LOG_WRN("Bad protocol mode write value %#x, ignoring", gatts_write->data[0]); + return; + } + + /* Store Protocol Mode of the host */ + ctx->protocol_mode = gatts_write->data[0]; + + if (hids->evt_handler != NULL) { + evt.ble_evt = ble_evt; + hids->evt_handler(hids, &evt); + } +} + +static void on_report_cccd_write(struct ble_hids *hids, struct ble_hids_char_id *char_id, + const ble_evt_t *ble_evt) +{ + const ble_gatts_evt_write_t *gatts_write = &ble_evt->evt.gatts_evt.params.write; + + LOG_INF("Report CCCD write"); + + if (!hids->evt_handler) { + return; + } + + struct ble_hids_evt evt = { + .evt_type = is_notification_enabled(gatts_write->data) + ? BLE_HIDS_EVT_NOTIF_ENABLED + : BLE_HIDS_EVT_NOTIF_DISABLED, + .params.notification.char_id = *char_id, + .ble_evt = ble_evt, + }; + + hids->evt_handler(hids, &evt); +} + +static void on_report_value_write(struct ble_hids *hids, const ble_evt_t *ble_evt, + struct ble_hids_char_id *char_id, void *dest, + uint16_t rep_max_len) +{ + const ble_gatts_evt_write_t *gatts_write = &ble_evt->evt.gatts_evt.params.write; + + LOG_DBG("Report value write"); + + /** TODO: SoftDevice should have checked this, confirm? */ + __ASSERT_NO_MSG(gatts_write->offset + gatts_write->len <= rep_max_len); + + /* Store the values in host's report data */ + memcpy(dest, gatts_write->data, gatts_write->len); + + if (!hids->evt_handler) { + return; + } + + struct ble_hids_evt evt = { + .evt_type = BLE_HIDS_EVT_REP_CHAR_WRITE, + .params.char_write.char_id = *char_id, + .params.char_write.offset = ble_evt->evt.gatts_evt.params.write.offset, + .params.char_write.len = ble_evt->evt.gatts_evt.params.write.len, + .params.char_write.data = ble_evt->evt.gatts_evt.params.write.data, + .ble_evt = ble_evt, + }; + + hids->evt_handler(hids, &evt); +} + +static void on_protocol_mode_read_auth(struct ble_hids *hids, const ble_gatts_evt_t *gatts_evt) +{ + uint32_t nrf_err; + struct ble_hids_context *ctx; + uint16_t read_offset; + + ctx = ble_hids_context_get(gatts_evt->conn_handle); + read_offset = gatts_evt->params.authorize_request.request.read.offset; + + /* Update GATTS table with this host's Protocol Mode value and authorize Read */ + const ble_gatts_rw_authorize_reply_params_t auth_read_params = { + .type = BLE_GATTS_AUTHORIZE_TYPE_READ, + .params.read = { + .gatt_status = BLE_GATT_STATUS_SUCCESS, + .len = sizeof(ctx->protocol_mode), + .p_data = &ctx->protocol_mode, + .offset = read_offset, + .update = 1, + }, + }; + + nrf_err = sd_ble_gatts_rw_authorize_reply(gatts_evt->conn_handle, &auth_read_params); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to authorize protocol mode read, nrf_error %#x", nrf_err); + } +} + +static void on_report_value_read_auth(struct ble_hids *hids, struct ble_hids_char_id *char_id, + const ble_gatts_evt_t *gatts_evt, void *report, + uint16_t rep_max_len) +{ + uint32_t nrf_err; + uint16_t read_offset; + + LOG_DBG("Report value read auth"); + + read_offset = gatts_evt->params.authorize_request.request.read.offset; + + /* Update Report GATTS table with host's current report data */ + const ble_gatts_rw_authorize_reply_params_t auth_read_params = { + .type = BLE_GATTS_AUTHORIZE_TYPE_READ, + .params.read = { + .gatt_status = BLE_GATT_STATUS_SUCCESS, + .len = rep_max_len - read_offset, + .p_data = (uint8_t *)report + read_offset, + .offset = read_offset, + .update = 1, + }, + }; + + nrf_err = sd_ble_gatts_rw_authorize_reply(gatts_evt->conn_handle, &auth_read_params); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to authorize report value read, nrf_error %#x", nrf_err); + } + + if (!hids->evt_handler) { + return; + } + + struct ble_hids_evt evt = { + .evt_type = BLE_HIDS_EVT_REPORT_READ, + .params.char_auth_read.char_id = *char_id, + }; + + hids->evt_handler(hids, &evt); +} + +static bool inp_rep_cccd_identify(struct ble_hids *hids, uint16_t handle, + struct ble_hids_char_id *char_id) +{ + LOG_DBG("Searching for input report CCCD"); + + for (size_t i = 0; i < ARRAY_SIZE(hids->input_report); i++) { + if (hids->input_report[i].char_handles.cccd_handle == handle) { + *char_id = make_char_id(BLE_UUID_REPORT_CHAR, + BLE_HIDS_REPORT_TYPE_INPUT, i); + LOG_DBG("Input report CCCD found, handle %#x", handle); + return true; + } + } + + return false; +} + +static bool rep_value_identify(struct ble_hids *hids, uint16_t conn_handle, uint16_t handle, + struct ble_hids_char_id *char_id, void **report, + uint16_t *report_max_len) +{ + struct ble_hids_context *ctx; + + ctx = ble_hids_context_get(conn_handle); + + LOG_DBG("Searching for report value"); + + for (size_t i = 0; i < hids->input_report_count; i++) { + if (handle == hids->input_report[i].char_handles.value_handle) { + *char_id = make_char_id(BLE_UUID_REPORT_CHAR, + BLE_HIDS_REPORT_TYPE_INPUT, i); + *report_max_len = hids->input_report[i].max_len; + *report = &ctx->input_reports[i]; + LOG_DBG("Input report %d handle %#x", i, handle); + return true; + } + } + for (size_t i = 0; i < hids->output_report_count; i++) { + if (handle == hids->output_report[i].char_handles.value_handle) { + *char_id = make_char_id(BLE_UUID_REPORT_CHAR, + BLE_HIDS_REPORT_TYPE_OUTPUT, i); + *report_max_len = hids->output_report[i].max_len; + *report = &ctx->output_reports[i]; + LOG_DBG("Output report %d handle %#x", i, handle); + return true; + } + } + for (size_t i = 0; i < hids->feature_report_count; i++) { + if (handle == hids->feature_report[i].char_handles.value_handle) { + *char_id = make_char_id(BLE_UUID_REPORT_CHAR, + BLE_HIDS_REPORT_TYPE_FEATURE, i); + *report_max_len = hids->feature_report[i].max_len; + *report = &ctx->feature_reports[i]; + LOG_DBG("Feature report %d handle %#x", i, handle); + return true; + } + } + + return false; +} + +static void on_write(struct ble_hids *hids, const ble_evt_t *ble_evt) +{ + struct ble_hids_context *ctx; + struct ble_hids_char_id char_id; + void *report; + uint16_t max_rep_len; + uint16_t conn_handle = ble_evt->evt.gatts_evt.conn_handle; + const ble_gatts_evt_write_t *gatts_write = &ble_evt->evt.gatts_evt.params.write; + + ctx = ble_hids_context_get(conn_handle); + + if (gatts_write->handle == hids->control_point_handles.value_handle) { + on_control_point_write(hids, ble_evt); + return; + } + if (gatts_write->handle == hids->protocol_mode_handles.value_handle) { + on_protocol_mode_write(hids, ble_evt); + return; + } + if (inp_rep_cccd_identify(hids, gatts_write->handle, &char_id)) { + on_report_cccd_write(hids, &char_id, ble_evt); + return; + } +#if CONFIG_BLE_HIDS_BOOT_KEYBOARD + if (gatts_write->handle == hids->boot_kb_inp_rep_handles.cccd_handle) { + LOG_INF("Boot Keyboard input report CCCD"); + char_id = make_char_id(BLE_UUID_BOOT_KEYBOARD_INPUT_REPORT_CHAR, 0, 0); + on_report_cccd_write(hids, &char_id, ble_evt); + return; + } + if (gatts_write->handle == hids->boot_kb_inp_rep_handles.value_handle) { + LOG_INF("Boot keyboard input report value"); + char_id = make_char_id(BLE_UUID_BOOT_KEYBOARD_INPUT_REPORT_CHAR, 0, 0); + on_report_value_write(hids, ble_evt, &char_id, ctx->boot_key_in_rep, + BLE_HIDS_BOOT_KB_INPUT_REP_MAX_SIZE); + return; + } + if (gatts_write->handle == hids->boot_kb_outp_rep_handles.value_handle) { + LOG_INF("Boot keyboard output report value"); + char_id = make_char_id(BLE_UUID_BOOT_KEYBOARD_OUTPUT_REPORT_CHAR, 0, 0); + on_report_value_write(hids, ble_evt, &char_id, ctx->boot_key_out_rep, + BLE_HIDS_BOOT_KB_OUTPUT_REP_MAX_SIZE); + return; + } +#endif +#if CONFIG_BLE_HIDS_BOOT_MOUSE + if (gatts_write->handle == hids->boot_mouse_inp_rep_handles.cccd_handle) { + LOG_INF("Boot Mouse input report CCCD"); + char_id = make_char_id(BLE_UUID_BOOT_MOUSE_INPUT_REPORT_CHAR, 0, 0); + on_report_cccd_write(hids, &char_id, ble_evt); + return; + } + if (gatts_write->handle == hids->boot_mouse_inp_rep_handles.value_handle) { + LOG_INF("Boot mouse input report value"); + char_id = make_char_id(BLE_UUID_BOOT_MOUSE_INPUT_REPORT_CHAR, 0, 0); + on_report_value_write(hids, ble_evt, &char_id, ctx->boot_mouse_in_rep, + BLE_HIDS_BOOT_MOUSE_INPUT_REP_MAX_SIZE); + return; + } +#endif + if (rep_value_identify(hids, conn_handle, gatts_write->handle, &char_id, &report, + &max_rep_len)) { + on_report_value_write(hids, ble_evt, &char_id, report, max_rep_len); + return; + } +} + +static void on_rw_authorize_request(struct ble_hids *hids, const ble_evt_t *ble_evt) +{ + void *report; + struct ble_hids_context *ctx; + uint16_t conn_handle; + uint16_t max_rep_len; + struct ble_hids_char_id char_id; + const ble_gatts_evt_rw_authorize_request_t *gatts_rw_auth = + &ble_evt->evt.gatts_evt.params.authorize_request; + + conn_handle = ble_evt->evt.common_evt.conn_handle; + + if (gatts_rw_auth->type != BLE_GATTS_AUTHORIZE_TYPE_READ) { + /* Unexpected operation */ + return; + } + + ctx = ble_hids_context_get(conn_handle); + + /* Update SD GATTS values of appropriate host before SD sends the Read Response */ + if (gatts_rw_auth->request.read.handle == hids->protocol_mode_handles.value_handle) { + on_protocol_mode_read_auth(hids, &ble_evt->evt.gatts_evt); + return; + } +#if CONFIG_BLE_HIDS_BOOT_KEYBOARD + if (gatts_rw_auth->request.read.handle == hids->boot_kb_inp_rep_handles.value_handle) { + char_id = make_char_id(BLE_UUID_BOOT_KEYBOARD_INPUT_REPORT_CHAR, 0, 0); + on_report_value_read_auth(hids, &char_id, &ble_evt->evt.gatts_evt, + ctx->boot_key_in_rep, sizeof(ctx->boot_key_in_rep)); + return; + } + if (gatts_rw_auth->request.read.handle == hids->boot_kb_outp_rep_handles.value_handle) { + char_id = make_char_id(BLE_UUID_BOOT_KEYBOARD_OUTPUT_REPORT_CHAR, 0, 0); + on_report_value_read_auth(hids, &char_id, &ble_evt->evt.gatts_evt, + ctx->boot_key_out_rep, sizeof(ctx->boot_key_out_rep)); + return; + } +#endif +#if CONFIG_BLE_HIDS_BOOT_MOUSE + if (gatts_rw_auth->request.read.handle == hids->boot_mouse_inp_rep_handles.value_handle) { + char_id = make_char_id(BLE_UUID_BOOT_MOUSE_INPUT_REPORT_CHAR, 0, 0); + on_report_value_read_auth(hids, &char_id, &ble_evt->evt.gatts_evt, + ctx->boot_mouse_in_rep, sizeof(ctx->boot_mouse_in_rep)); + return; + } +#endif + if (rep_value_identify(hids, conn_handle, gatts_rw_auth->request.read.handle, &char_id, + &report, &max_rep_len)) { + on_report_value_read_auth(hids, &char_id, &ble_evt->evt.gatts_evt, + report, max_rep_len); + return; + } +} + +void ble_hids_on_ble_evt(const ble_evt_t *ble_evt, void *p_context) +{ + struct ble_hids *hids = (struct ble_hids *)p_context; + + switch (ble_evt->header.evt_id) { + case BLE_GAP_EVT_CONNECTED: + on_connected(hids, ble_evt); + break; + case BLE_GAP_EVT_DISCONNECTED: + on_disconnected(hids, ble_evt); + break; + case BLE_GATTS_EVT_WRITE: + on_write(hids, ble_evt); + break; + case BLE_GATTS_EVT_RW_AUTHORIZE_REQUEST: + on_rw_authorize_request(hids, ble_evt); + break; + default: + break; + } +} + +#if defined(CONFIG_BLE_HIDS_BOOT_KEYBOARD) || defined(CONFIG_BLE_HIDS_BOOT_MOUSE) +static uint32_t protocol_mode_char_add(struct ble_hids *hids) +{ + uint32_t nrf_err; + const ble_gatts_char_md_t char_md = { + .char_props = { .read = true, .write_wo_resp = true, }, + }; + const ble_gatts_attr_t char_params = { + .p_uuid = &(ble_uuid_t) { + .type = BLE_UUID_TYPE_BLE, + .uuid = BLE_UUID_PROTOCOL_MODE_CHAR, + }, + .p_attr_md = &(ble_gatts_attr_md_t) { + .vloc = BLE_GATTS_VLOC_STACK, + .rd_auth = true, + .read_perm = gap_conn_sec_mode_from_u8( + CONFIG_BLE_HIDS_PROTOCOL_MODE_CHAR_READ_SEC_MODE), + .write_perm = gap_conn_sec_mode_from_u8( + CONFIG_BLE_HIDS_PROTOCOL_MODE_CHAR_WRITE_SEC_MODE), + }, + .init_len = sizeof(uint8_t), + .max_len = sizeof(uint8_t), + .p_value = &(uint8_t){CONFIG_BLE_HIDS_DEFAULT_PROTOCOL_MODE}, + }; + + /* Add characteristic */ + nrf_err = sd_ble_gatts_characteristic_add(hids->service_handle, &char_md, &char_params, + &hids->protocol_mode_handles); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to add protocol mode characteristic, nrf_error %#x", nrf_err); + return nrf_err; + } + + return NRF_SUCCESS; +} +#endif + +static uint32_t rep_char_add(struct ble_hids *hids, struct ble_hids_report *report, + const struct ble_hids_report_config *report_init, + const ble_gatt_char_props_t *props) +{ + uint32_t nrf_err; + ble_gatts_char_md_t char_md = { + .char_props = *props, + }; + + if (char_md.char_props.notify || char_md.char_props.notify) { + /* There seems to be bug in gcc v12.2.0 where this structure is + * optimized in a bad way and not stored in memory correctly, + * thus the volatile keyword. + */ + volatile const ble_gatts_attr_md_t cccd_md = { + .vloc = BLE_GATTS_VLOC_STACK, + .read_perm = BLE_GAP_CONN_SEC_MODE_OPEN, + .write_perm = report_init->sec.cccd_write, + }; + + char_md.p_cccd_md = (ble_gatts_attr_md_t *)&cccd_md; + } + + const ble_gatts_attr_t char_params = { + .p_uuid = &(ble_uuid_t) { + .type = BLE_UUID_TYPE_BLE, + .uuid = BLE_UUID_REPORT_CHAR, + }, + .p_attr_md = &(ble_gatts_attr_md_t) { + .vlen = true, + .vloc = BLE_GATTS_VLOC_STACK, + .rd_auth = true, + .read_perm = report_init->sec.read, + .write_perm = report_init->sec.write, + }, + .max_len = report_init->len, + }; + + /* Add characteristic */ + nrf_err = sd_ble_gatts_characteristic_add(hids->service_handle, &char_md, &char_params, + &report->char_handles); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to add report characteristic, nrf_error %#x", nrf_err); + return nrf_err; + } + + /* Store the characteristic maximum length */ + report->max_len = report_init->len; + + const ble_gatts_attr_t descr_params = { + .p_uuid = &(ble_uuid_t){ + .type = BLE_UUID_TYPE_BLE, + .uuid = BLE_UUID_REPORT_REF_DESCR, + }, + .p_attr_md = &(ble_gatts_attr_md_t){ + .vloc = BLE_GATTS_VLOC_STACK, + .read_perm = report_init->sec.read, + .write_perm = report_init->sec.write, + }, + .init_len = sizeof(uint16_t), + .max_len = sizeof(uint16_t), + .p_value = (uint8_t[]) { + [0] = report_init->report_id, + [1] = report_init->report_type, + }, + }; + + /* Add the descriptor */ + nrf_err = sd_ble_gatts_descriptor_add(report->char_handles.value_handle, &descr_params, + &report->ref_handle); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to add GATT report reference descriptor, nrf_error %#x", nrf_err); + return nrf_err; + } + + return NRF_SUCCESS; +} + +static uint32_t report_map_char_add(struct ble_hids *hids, + const struct ble_hids_config *hids_config) +{ + uint32_t nrf_err; + const ble_gatts_char_md_t char_md = { + .char_props = { + .read = true, + }, + }; + const ble_gatts_attr_t char_params = { + .p_uuid = &(ble_uuid_t) { + .type = BLE_UUID_TYPE_BLE, + .uuid = BLE_UUID_REPORT_MAP_CHAR, + }, + .p_attr_md = &(ble_gatts_attr_md_t) { + .vlen = true, + .vloc = BLE_GATTS_VLOC_STACK, + .read_perm = hids_config->report_map.sec.read, + }, + .max_len = hids_config->report_map.len, + .init_len = hids_config->report_map.len, + .p_value = hids_config->report_map.data, + }; + + /* Add characteristic */ + nrf_err = sd_ble_gatts_characteristic_add(hids->service_handle, &char_md, &char_params, + &hids->rep_map_handles); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to add GATT characteristic, nrf_error %#x", nrf_err); + return nrf_err; + } + + /** TODO why here? */ + if (hids_config->report_map.ext_rep_ref_count != 0 && + hids_config->report_map.ext_rep_ref == NULL) { + return NRF_ERROR_INVALID_PARAM; + } + + for (size_t i = 0; i < hids_config->report_map.ext_rep_ref_count; ++i) { + uint8_t encoded_rep_ref[sizeof(ble_uuid128_t)]; + uint8_t encoded_rep_ref_len; + + /* Add External Report Reference descriptor */ + nrf_err = sd_ble_uuid_encode(&hids_config->report_map.ext_rep_ref[i], + &encoded_rep_ref_len, encoded_rep_ref); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + + const ble_gatts_attr_t descr_params = { + .p_uuid = &(ble_uuid_t){ + .type = BLE_UUID_TYPE_BLE, + .uuid = BLE_UUID_EXTERNAL_REPORT_REF_DESCR, + }, + .p_attr_md = &(ble_gatts_attr_md_t){ + .vloc = BLE_GATTS_VLOC_STACK, + .read_perm = hids_config->report_map.sec.read, + }, + .init_len = encoded_rep_ref_len, + .max_len = hids_config->report_map.len, + .p_value = encoded_rep_ref + }; + + /* Add the descriptor */ + nrf_err = sd_ble_gatts_descriptor_add(hids->rep_map_handles.value_handle, + &descr_params, &(uint16_t){0} /* discard*/); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to add GATT report reference descriptor, nrf_error %#x", + nrf_err); + return nrf_err; + } + } + + return NRF_SUCCESS; +} + +#if CONFIG_BLE_HIDS_BOOT_KEYBOARD +static uint32_t boot_kb_input_report_char_add(struct ble_hids *hids) +{ + uint32_t nrf_err; + const ble_gatts_char_md_t char_md = { + .char_props = { + .read = true, + .notify = true, + /* for debug */ + .write = true, + }, + .p_cccd_md = &(ble_gatts_attr_md_t) { + .vloc = BLE_GATTS_VLOC_STACK, + /* Use the same read permission as the characteristic's */ + .read_perm = gap_conn_sec_mode_from_u8( + CONFIG_BLE_HIDS_BOOT_KEYBOARD_INPUT_CHAR_READ_SEC_MODE), + .write_perm = gap_conn_sec_mode_from_u8( + CONFIG_BLE_HIDS_BOOT_KEYBOARD_INPUT_CCCD_WRITE_SEC_MODE), + }, + }; + const ble_gatts_attr_t char_params = { + .p_uuid = &(ble_uuid_t) { + .type = BLE_UUID_TYPE_BLE, + .uuid = BLE_UUID_BOOT_KEYBOARD_INPUT_REPORT_CHAR, + }, + .p_attr_md = &(ble_gatts_attr_md_t) { + .vloc = BLE_GATTS_VLOC_STACK, + .rd_auth = true, + .read_perm = gap_conn_sec_mode_from_u8( + CONFIG_BLE_HIDS_BOOT_KEYBOARD_INPUT_CHAR_READ_SEC_MODE), + /* for debug */ + .write_perm = BLE_GAP_CONN_SEC_MODE_OPEN, + }, + .max_len = BLE_HIDS_BOOT_KB_INPUT_REP_MAX_SIZE, + }; + + /* Add characteristic */ + nrf_err = sd_ble_gatts_characteristic_add(hids->service_handle, &char_md, &char_params, + &hids->boot_kb_inp_rep_handles); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to add Boot Keyboard input characteristic, nrf_error %#x", nrf_err); + return nrf_err; + } + + return NRF_SUCCESS; +} + +static uint32_t boot_kb_output_report_char_add(struct ble_hids *hids) +{ + uint32_t nrf_err; + const ble_gatts_char_md_t char_md = { + .char_props = { + .read = true, + .write = true, + .write_wo_resp = true, + }, + }; + const ble_gatts_attr_t char_params = { + .p_uuid = &(ble_uuid_t) { + .type = BLE_UUID_TYPE_BLE, + .uuid = BLE_UUID_BOOT_KEYBOARD_OUTPUT_REPORT_CHAR, + }, + .p_attr_md = &(ble_gatts_attr_md_t) { + .vloc = BLE_GATTS_VLOC_STACK, + .rd_auth = true, + .read_perm = gap_conn_sec_mode_from_u8( + CONFIG_BLE_HIDS_BOOT_KEYBOARD_OUTPUT_CHAR_READ_SEC_MODE), + .write_perm = gap_conn_sec_mode_from_u8( + CONFIG_BLE_HIDS_BOOT_KEYBOARD_OUTPUT_CHAR_WRITE_SEC_MODE), + }, + .max_len = BLE_HIDS_BOOT_KB_INPUT_REP_MAX_SIZE, + }; + + /* Add characteristic */ + nrf_err = sd_ble_gatts_characteristic_add(hids->service_handle, &char_md, &char_params, + &hids->boot_kb_outp_rep_handles); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to add Boot Keyboard output char, nrf_error %#x", nrf_err); + return nrf_err; + } + + return NRF_SUCCESS; +} +#endif + +#if CONFIG_BLE_HIDS_BOOT_MOUSE +static uint32_t boot_mouse_input_report_char_add(struct ble_hids *hids) +{ + uint32_t nrf_err; + const ble_gatts_char_md_t char_md = { + .char_props = { + .read = true, + .notify = true, + /* for debug */ + .write = true, + }, + .p_cccd_md = &(ble_gatts_attr_md_t) { + .vloc = BLE_GATTS_VLOC_STACK, + /* Use the same read permission as the characteristic's */ + .read_perm = gap_conn_sec_mode_from_u8( + CONFIG_BLE_HIDS_BOOT_MOUSE_INPUT_CHAR_READ_SEC_MODE), + .write_perm = gap_conn_sec_mode_from_u8( + CONFIG_BLE_HIDS_BOOT_MOUSE_INPUT_CCCD_WRITE_SEC_MODE), + }, + }; + const ble_gatts_attr_t char_params = { + .p_uuid = &(ble_uuid_t) { + .type = BLE_UUID_TYPE_BLE, + .uuid = BLE_UUID_BOOT_MOUSE_INPUT_REPORT_CHAR, + }, + .p_attr_md = &(ble_gatts_attr_md_t) { + .vloc = BLE_GATTS_VLOC_STACK, + .rd_auth = true, + .read_perm = gap_conn_sec_mode_from_u8( + CONFIG_BLE_HIDS_BOOT_MOUSE_INPUT_CHAR_READ_SEC_MODE), + /* for debug */ + .write_perm = BLE_GAP_CONN_SEC_MODE_OPEN, + }, + .max_len = BLE_HIDS_BOOT_KB_INPUT_REP_MAX_SIZE, + }; + + /* Add characteristic */ + nrf_err = sd_ble_gatts_characteristic_add(hids->service_handle, &char_md, &char_params, + &hids->boot_mouse_inp_rep_handles); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to add GATT characteristic, nrf_error %#x", nrf_err); + return nrf_err; + } + + return NRF_SUCCESS; +} +#endif + +static uint32_t hid_information_char_add(struct ble_hids *hids, + const struct ble_hids_config *hids_config) +{ + uint32_t nrf_err; + const ble_gatts_char_md_t char_md = { + .char_props = { .read = true, }, + }; + const ble_gatts_attr_t char_params = { + .p_uuid = &(ble_uuid_t){ + .type = BLE_UUID_TYPE_BLE, + .uuid = BLE_UUID_HID_INFORMATION_CHAR, + }, + .p_attr_md = &(ble_gatts_attr_md_t) { + .vloc = BLE_GATTS_VLOC_STACK, + .read_perm = gap_conn_sec_mode_from_u8( + CONFIG_BLE_HIDS_INFORMATION_CHAR_READ_SEC_MODE) + }, + .init_len = sizeof(hids_config->hid_information), + .max_len = sizeof(hids_config->hid_information), + .p_value = (uint8_t *)&hids_config->hid_information, + }; + + /* Add characteristic */ + nrf_err = sd_ble_gatts_characteristic_add(hids->service_handle, &char_md, &char_params, + &hids->hid_information_handles); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to add GATT characteristic, nrf_error %#x", nrf_err); + return nrf_err; + } + + return NRF_SUCCESS; +} + +static uint32_t hid_control_point_char_add(struct ble_hids *hids) +{ + uint32_t nrf_err; + const ble_gatts_char_md_t char_md = { + .char_props = { .write_wo_resp = true }, + }; + const ble_gatts_attr_t char_params = { + .p_uuid = &(ble_uuid_t){ + .type = BLE_UUID_TYPE_BLE, + .uuid = BLE_UUID_HID_CONTROL_POINT_CHAR, + }, + .p_attr_md = &(ble_gatts_attr_md_t){ + .vloc = BLE_GATTS_VLOC_STACK, + .write_perm = gap_conn_sec_mode_from_u8( + CONFIG_BLE_HIDS_CONTROL_POINT_CHAR_WRITE_SEC_MODE), + }, + .init_len = sizeof(uint8_t), + .max_len = sizeof(uint8_t), + .p_value = &(uint8_t){CONTROL_POINT_EXIT_SUSPEND}, + }; + + /* Add characteristic */ + nrf_err = sd_ble_gatts_characteristic_add(hids->service_handle, &char_md, &char_params, + &hids->control_point_handles); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to add GATT characteristic, nrf_error %#x", nrf_err); + return nrf_err; + } + + return NRF_SUCCESS; +} + +static uint32_t input_report_char_add(struct ble_hids *hids, + const struct ble_hids_config *hids_config) +{ + uint32_t nrf_err; + + if ((hids_config->input_report_count == 0 && hids_config->input_report != NULL) || + (hids_config->input_report_count != 0 && hids_config->input_report == NULL)) { + return NRF_ERROR_INVALID_PARAM; + } + + for (size_t i = 0; i < hids_config->input_report_count; i++) { + const struct ble_hids_report_config *report = &hids_config->input_report[i]; + const ble_gatt_char_props_t properties = { + .read = true, + .notify = true, + .write = !ble_gap_conn_sec_mode_equal( + &report->sec.write, &BLE_GAP_CONN_SEC_MODE_NO_ACCESS), + }; + + nrf_err = rep_char_add(hids, &hids->input_report[i], report, &properties); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + } + + hids->input_report_count = hids_config->input_report_count; + + LOG_DBG("Input report characteristics added"); + + return NRF_SUCCESS; +} + +static uint32_t output_report_char_add(struct ble_hids *hids, + const struct ble_hids_config *hids_config) +{ + uint32_t nrf_err; + + if ((hids_config->output_report_count == 0 && hids_config->output_report != NULL) || + (hids_config->output_report_count != 0 && hids_config->output_report == NULL)) { + return NRF_ERROR_INVALID_PARAM; + } + + for (size_t i = 0; i < hids_config->output_report_count; i++) { + const struct ble_hids_report_config *report = &hids_config->output_report[i]; + const ble_gatt_char_props_t properties = { + .read = true, + .write = true, + .write_wo_resp = true, + }; + + nrf_err = rep_char_add(hids, &hids->output_report[i], report, &properties); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + } + + hids->output_report_count = hids_config->output_report_count; + + LOG_DBG("Output report characteristics added"); + + return NRF_SUCCESS; +} + +static uint32_t feature_report_char_add(struct ble_hids *hids, + const struct ble_hids_config *hids_config) +{ + uint32_t nrf_err; + + if ((hids_config->feature_report_count == 0 && hids_config->feature_report != NULL) || + (hids_config->feature_report_count != 0 && hids_config->feature_report == NULL)) { + return NRF_ERROR_INVALID_PARAM; + } + + for (size_t i = 0; i < hids_config->feature_report_count; i++) { + const struct ble_hids_report_config *report = &hids_config->feature_report[i]; + const ble_gatt_char_props_t properties = { + .read = true, + .write = true, + }; + + nrf_err = rep_char_add(hids, &hids->feature_report[i], report, &properties); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + } + + hids->feature_report_count = hids_config->feature_report_count; + + LOG_DBG("Feature report characteristics added"); + + return NRF_SUCCESS; +} + +static uint32_t includes_add(struct ble_hids *hids, const struct ble_hids_config *hids_config) +{ + uint32_t nrf_err; + uint16_t unused_handle; + + for (size_t i = 0; i < hids_config->included_services_count; i++) { + nrf_err = sd_ble_gatts_include_add(hids->service_handle, + hids_config->included_services_array[i], + &unused_handle); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + } + + return NRF_SUCCESS; +} + +uint32_t ble_hids_init(struct ble_hids *hids, const struct ble_hids_config *hids_config) +{ + uint32_t nrf_err; + ble_uuid_t ble_uuid = { + .type = BLE_UUID_TYPE_BLE, + .uuid = BLE_UUID_HUMAN_INTERFACE_DEVICE_SERVICE, + }; + + if (!hids || !hids_config) { + return NRF_ERROR_NULL; + } + if ((hids_config->input_report_count > CONFIG_BLE_HIDS_MAX_INPUT_REP) || + (hids_config->output_report_count > CONFIG_BLE_HIDS_MAX_OUTPUT_REP) || + (hids_config->feature_report_count > CONFIG_BLE_HIDS_MAX_FEATURE_REP)) { + return NRF_ERROR_INVALID_PARAM; + } + + /* Add service */ + nrf_err = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, + &hids->service_handle); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + + /* Add includes */ + nrf_err = includes_add(hids, hids_config); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + + if (IS_ENABLED(CONFIG_BLE_HIDS_BOOT_KEYBOARD) || IS_ENABLED(CONFIG_BLE_HIDS_BOOT_MOUSE)) { + /* Add Protocol Mode characteristic */ + nrf_err = protocol_mode_char_add(hids); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + } + + /* Add Input Report characteristics (if any) */ + nrf_err = input_report_char_add(hids, hids_config); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + + /* Add Output Report characteristics (if any) */ + nrf_err = output_report_char_add(hids, hids_config); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + + /* Add Feature Report characteristic (if any) */ + nrf_err = feature_report_char_add(hids, hids_config); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + + /* Add Report Map characteristic */ + nrf_err = report_map_char_add(hids, hids_config); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + +#if CONFIG_BLE_HIDS_BOOT_KEYBOARD + nrf_err = boot_kb_input_report_char_add(hids); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + + nrf_err = boot_kb_output_report_char_add(hids); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } +#endif + +#if CONFIG_BLE_HIDS_BOOT_MOUSE + nrf_err = boot_mouse_input_report_char_add(hids); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } +#endif + + /* Add HID Information characteristic */ + nrf_err = hid_information_char_add(hids, hids_config); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + + /* Add HID Control Point characteristic */ + nrf_err = hid_control_point_char_add(hids); + if (nrf_err != NRF_SUCCESS) { + return nrf_err; + } + + LOG_INF("BLE HID service initialized"); + LOG_DBG("Size of HID client context is %u bytes", sizeof(struct ble_hids_context)); + + hids->evt_handler = hids_config->evt_handler; + + return NRF_SUCCESS; +} + +uint32_t ble_hids_event_handler_set(struct ble_hids *hids, ble_hids_evt_handler_t handler) +{ + if (!hids || !handler) { + return NRF_ERROR_NULL; + } + + hids->evt_handler = handler; + + return NRF_SUCCESS; +} + +uint32_t ble_hids_inp_rep_send(struct ble_hids *hids, uint16_t conn_handle, uint8_t rep_index, + const void *data, const uint16_t len) +{ + uint32_t nrf_err; + void *rep_data; + struct ble_hids_context *ctx; + + if (!hids || !data) { + return NRF_ERROR_NULL; + } + + ctx = ble_hids_context_get(conn_handle); + if (!ctx) { + return NRF_ERROR_NOT_FOUND; + } + + if (rep_index > ARRAY_SIZE(ctx->input_reports)) { + LOG_ERR("Invalid report index %u (total: %u)", + rep_index, ARRAY_SIZE(ctx->input_reports)); + return NRF_ERROR_INVALID_PARAM; + } + /** TODO: is this caught from sd_ble_gatts_hvx() ? */ + if (len > hids->input_report[rep_index].max_len) { + LOG_ERR("Report is too big to fit in host data"); + return NRF_ERROR_DATA_SIZE; + } + + /* Store the new report data in host's context */ + rep_data = ctx->input_reports[rep_index].data; + memcpy(rep_data, data, len); + + const ble_gatts_hvx_params_t hvx = { + .handle = hids->input_report[rep_index].char_handles.value_handle, + .type = BLE_GATT_HVX_NOTIFICATION, + .p_len = &(uint16_t){len}, + .p_data = data, + }; + + nrf_err = sd_ble_gatts_hvx(conn_handle, &hvx); + if ((nrf_err == NRF_SUCCESS) && (*hvx.p_len != len)) { + LOG_ERR("Wrong notification data size"); + return NRF_ERROR_INVALID_PARAM; + } + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to send notification, nrf_error %#x", nrf_err); + return NRF_ERROR_INVALID_PARAM; + } + + LOG_DBG("Input report notification sent"); + + return NRF_SUCCESS; +} + +uint32_t ble_hids_boot_kb_inp_rep_send(struct ble_hids *hids, uint16_t conn_handle, + const struct ble_hids_boot_keyboard_input_report *report) +{ +#if CONFIG_BLE_HIDS_BOOT_KEYBOARD + uint32_t nrf_err; + struct ble_hids_context *ctx; + + if (!hids || !report) { + return NRF_ERROR_NULL; + } + + ctx = ble_hids_context_get(conn_handle); + if (!ctx) { + return NRF_ERROR_NOT_FOUND; + } + + /* Copy into context */ + memcpy(ctx->boot_key_in_rep, report, sizeof(ctx->boot_key_in_rep)); + + const ble_gatts_hvx_params_t hvx = { + .handle = hids->boot_kb_inp_rep_handles.value_handle, + .type = BLE_GATT_HVX_NOTIFICATION, + .p_len = &(uint16_t){sizeof(ctx->boot_key_in_rep)}, + .p_data = ctx->boot_key_in_rep, + }; + + nrf_err = sd_ble_gatts_hvx(conn_handle, &hvx); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to send notification, nrf_error %#x", nrf_err); + return NRF_ERROR_INVALID_PARAM; + } + + LOG_DBG("Boot keyboard input report sent"); + + return NRF_SUCCESS; +#else + return NRF_ERROR_FORBIDDEN; +#endif +} + +uint32_t ble_hids_boot_mouse_inp_rep_send(struct ble_hids *hids, uint16_t conn_handle, + const struct ble_hids_boot_mouse_input_report *report) +{ +#if CONFIG_BLE_HIDS_BOOT_MOUSE + uint32_t nrf_err; + struct ble_hids_context *ctx; + + if (!hids || !report) { + return NRF_ERROR_NULL; + } + + ctx = ble_hids_context_get(conn_handle); + if (!ctx) { + return NRF_ERROR_NOT_FOUND; + } + + memcpy(&ctx->boot_mouse_in_rep, report, sizeof(ctx->boot_mouse_in_rep)); + + const ble_gatts_hvx_params_t hvx = { + .handle = hids->boot_mouse_inp_rep_handles.value_handle, + .type = BLE_GATT_HVX_NOTIFICATION, + .p_len = &(uint16_t){BLE_HIDS_BOOT_MOUSE_INPUT_REP_MAX_SIZE}, + .p_data = ctx->boot_mouse_in_rep, + }; + + nrf_err = sd_ble_gatts_hvx(conn_handle, &hvx); + if (nrf_err != NRF_SUCCESS) { + LOG_ERR("Failed to send notification, nrf_error %#x", nrf_err); + return NRF_ERROR_INVALID_PARAM; + } + + LOG_DBG("Boot mouse input report sent"); + + return NRF_SUCCESS; +#else + return NRF_ERROR_FORBIDDEN; +#endif +} + +uint32_t ble_hids_outp_rep_get(struct ble_hids *hids, uint8_t rep_index, uint16_t len, + uint8_t offset, uint16_t conn_handle, uint8_t *outp_rep) +{ + void *src; + struct ble_hids_context *ctx; + + if (!hids || !outp_rep) { + return NRF_ERROR_NULL; + } + if (rep_index >= ARRAY_SIZE(hids->output_report)) { + return NRF_ERROR_INVALID_PARAM; + } + + ctx = ble_hids_context_get(conn_handle); + if (!ctx) { + return NRF_ERROR_NOT_FOUND; + } + + if (offset + len > hids->output_report[rep_index].max_len) { + LOG_ERR("Output buffer too small for report data"); + return NRF_ERROR_DATA_SIZE; + } + + /* Copy the report data into user provided buffer */ + src = ctx->output_reports[rep_index].data; + memcpy(outp_rep, (uint8_t *)src + offset, len); + + return NRF_SUCCESS; +}