Testing: split input test (#2762)

test(pointing): Add mock input device.

New mock input device to generate input events for tests.

test(split): Add peripheral input test.

Test event propagation from peripheral input devices.

fix(split): Proper scoping for local within switch.

Minor compile fix.

chore: Fix up test snapshots after logging changes.

Adjust the test snapshots after logging changes to the central.

fix(kscan): Don't fire last mock event twice.

Fix a bug where the kscan mock would raise the last mock event
twice before halting processing.
This commit is contained in:
Pete Johanson 2025-01-13 13:15:16 -07:00 committed by GitHub
parent 022603ec16
commit 8dddb1d9d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 332 additions and 21 deletions

View File

@ -5,3 +5,4 @@ add_subdirectory_ifdef(CONFIG_GPIO gpio)
add_subdirectory_ifdef(CONFIG_KSCAN kscan)
add_subdirectory_ifdef(CONFIG_SENSOR sensor)
add_subdirectory_ifdef(CONFIG_DISPLAY display)
add_subdirectory_ifdef(CONFIG_INPUT input)

View File

@ -5,3 +5,4 @@ rsource "gpio/Kconfig"
rsource "kscan/Kconfig"
rsource "sensor/Kconfig"
rsource "display/Kconfig"
rsource "input/Kconfig"

View File

@ -0,0 +1,6 @@
# Copyright (c) 2020-2023 The ZMK Contributors
# SPDX-License-Identifier: MIT
zephyr_library_amend()
zephyr_library_sources_ifdef(CONFIG_ZMK_INPUT_MOCK input_mock.c)

View File

@ -0,0 +1,9 @@
if INPUT
config ZMK_INPUT_MOCK
bool "Input Mock"
default y
depends on DT_HAS_ZMK_INPUT_MOCK_ENABLED
endif

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT zmk_input_mock
#include <stdlib.h>
#include <zephyr/device.h>
#include <zephyr/input/input.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
struct input_mock_config {
uint16_t startup_delay;
uint16_t event_period;
bool exit_after;
const uint32_t *events;
size_t events_len;
};
struct input_mock_data {
size_t event_index;
struct k_work_delayable work;
const struct device *dev;
};
static void input_mock_work_cb(struct k_work *work) {
struct k_work_delayable *dwork = CONTAINER_OF(work, struct k_work_delayable, work);
struct input_mock_data *data = CONTAINER_OF(dwork, struct input_mock_data, work);
const struct device *dev = data->dev;
const struct input_mock_config *cfg = dev->config;
data->event_index++;
size_t base_idx = data->event_index * 4;
if (base_idx >= cfg->events_len) {
if (cfg->exit_after) {
exit(0);
} else {
return;
}
}
input_report(dev, cfg->events[base_idx], cfg->events[base_idx + 1], cfg->events[base_idx + 2],
cfg->events[base_idx + 3], K_NO_WAIT);
k_work_schedule(&data->work, K_MSEC(cfg->event_period));
}
int input_mock_init(const struct device *dev) {
struct input_mock_data *drv_data = dev->data;
const struct input_mock_config *drv_cfg = dev->config;
drv_data->dev = dev;
drv_data->event_index = -1;
k_work_init_delayable(&drv_data->work, input_mock_work_cb);
k_work_schedule(&drv_data->work, K_MSEC(drv_cfg->startup_delay));
return 0;
}
#define GET_EVENT(n, inst) \
{}
#define INPUT_MOCK_INST(n) \
struct input_mock_data input_mock_data_##n = {}; \
const uint32_t mock_data_##n[] = DT_INST_PROP(n, events); \
const struct input_mock_config input_mock_cfg_##n = { \
.events = mock_data_##n, \
.events_len = DT_INST_PROP_LEN(n, events), \
.startup_delay = DT_INST_PROP(n, event_startup_delay), \
.event_period = DT_INST_PROP(n, event_period), \
.exit_after = DT_INST_PROP(n, exit_after), \
}; \
DEVICE_DT_INST_DEFINE(n, input_mock_init, NULL, &input_mock_data_##n, &input_mock_cfg_##n, \
POST_KERNEL, CONFIG_INPUT_INIT_PRIORITY, NULL);
DT_INST_FOREACH_STATUS_OKAY(INPUT_MOCK_INST)

View File

@ -65,6 +65,12 @@ static int kscan_mock_configure(const struct device *dev, kscan_callback_t callb
struct k_work_delayable *d_work = k_work_delayable_from_work(work); \
struct kscan_mock_data *data = CONTAINER_OF(d_work, struct kscan_mock_data, work); \
const struct kscan_mock_config_##n *cfg = data->dev->config; \
if (data->event_index >= DT_INST_PROP_LEN(n, events)) { \
if (cfg->exit_after) \
exit(0); \
else \
return; \
} \
uint32_t ev = cfg->events[data->event_index]; \
LOG_DBG("ev %u row %d column %d state %d\n", ev, ZMK_MOCK_ROW(ev), ZMK_MOCK_COL(ev), \
ZMK_MOCK_IS_PRESS(ev)); \

View File

@ -0,0 +1,21 @@
# Copyright (c) 2024 The ZMK Contributors
# SPDX-License-Identifier: MIT
description: |
Allows defining a mock input driver that simulates periodic input events.
compatible: "zmk,input-mock"
properties:
event-startup-delay:
type: int
default: 0
description: Milliseconds to delay before starting generating the events
event-period:
type: int
description: Milliseconds between each generated event
events:
type: array
description: List of tuples of (type, code, value, sync)
exit-after:
type: boolean

View File

@ -582,7 +582,7 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn,
LOG_DBG("[ATTRIBUTE] handle %u", attr->handle);
switch (params->type) {
case BT_GATT_DISCOVER_CHARACTERISTIC:
case BT_GATT_DISCOVER_CHARACTERISTIC: {
const struct bt_uuid *chrc_uuid = ((struct bt_gatt_chrc *)attr->user_data)->uuid;
if (bt_uuid_cmp(chrc_uuid, BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_POSITION_STATE_UUID)) ==
@ -665,6 +665,7 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn,
#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) */
}
break;
}
case BT_GATT_DISCOVER_STD_CHAR_DESC:
#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
if (bt_uuid_cmp(slot->discover_params.uuid, BT_UUID_GATT_CCC) == 0) {

View File

@ -45,6 +45,7 @@ static bool skip_set_security_on_connect = false;
static bool skip_discovery_on_connect = false;
static bool read_directly_on_discovery = false;
static bool write_hid_indicators_on_discovery = false;
static bool subscribe_to_pointer_report = false;
static int32_t wait_on_start = 0;
static void ble_central_native_posix_options(void) {
@ -74,6 +75,11 @@ static void ble_central_native_posix_options(void) {
.type = 'b',
.dest = (void *)&read_hid_report_on_connect,
.descript = "Read the peripheral HID report after connecting"},
{.is_switch = true,
.option = "subscribe_to_pointer_report",
.type = 'b',
.dest = (void *)&subscribe_to_pointer_report,
.descript = "Subscribe to peripheral mouse HID report after connecting"},
{.is_switch = true,
.option = "skip_discovery_on_connect",
.type = 'b',
@ -110,6 +116,8 @@ static struct bt_conn *default_conn;
static struct bt_uuid_16 uuid = BT_UUID_INIT_16(0);
static struct bt_gatt_discover_params discover_params;
static struct bt_gatt_subscribe_params subscribe_params;
static struct bt_gatt_subscribe_params consumer_subscribe_params;
static struct bt_gatt_subscribe_params pointer_subscribe_params;
static uint8_t notify_func(struct bt_conn *conn, struct bt_gatt_subscribe_params *params,
const void *data, uint16_t length) {
@ -167,6 +175,28 @@ static uint8_t discover_func(struct bt_conn *conn, const struct bt_gatt_attr *at
discover_params.type = BT_GATT_DISCOVER_DESCRIPTOR;
subscribe_params.value_handle = bt_gatt_attr_value_handle(attr);
err = bt_gatt_discover(conn, &discover_params);
if (err) {
LOG_DBG("[Discover failed] (err %d)", err);
}
} else if (!consumer_subscribe_params.value_handle) {
memcpy(&uuid, BT_UUID_GATT_CCC, sizeof(uuid));
discover_params.uuid = &uuid.uuid;
discover_params.start_handle = attr->handle + 2;
discover_params.type = BT_GATT_DISCOVER_DESCRIPTOR;
consumer_subscribe_params.value_handle = bt_gatt_attr_value_handle(attr);
err = bt_gatt_discover(conn, &discover_params);
if (err) {
LOG_DBG("[Discover failed] (err %d)", err);
}
} else if (subscribe_to_pointer_report && !pointer_subscribe_params.value_handle) {
memcpy(&uuid, BT_UUID_GATT_CCC, sizeof(uuid));
discover_params.uuid = &uuid.uuid;
discover_params.start_handle = attr->handle + 2;
discover_params.type = BT_GATT_DISCOVER_DESCRIPTOR;
pointer_subscribe_params.value_handle = bt_gatt_attr_value_handle(attr);
err = bt_gatt_discover(conn, &discover_params);
if (err) {
LOG_DBG("[Discover failed] (err %d)", err);
@ -182,18 +212,45 @@ static uint8_t discover_func(struct bt_conn *conn, const struct bt_gatt_attr *at
return BT_GATT_ITER_CONTINUE;
}
} else if (discover_params.type == BT_GATT_DISCOVER_DESCRIPTOR) {
subscribe_params.notify = notify_func;
subscribe_params.value = BT_GATT_CCC_NOTIFY;
subscribe_params.ccc_handle = attr->handle;
if (!subscribe_params.ccc_handle) {
err = bt_gatt_subscribe(conn, &subscribe_params);
if (err && err != -EALREADY) {
LOG_DBG("[Subscribe failed] (err %d)", err);
subscribe_params.notify = notify_func;
subscribe_params.value = BT_GATT_CCC_NOTIFY;
subscribe_params.ccc_handle = attr->handle;
err = bt_gatt_subscribe(conn, &subscribe_params);
if (err && err != -EALREADY) {
LOG_DBG("[Subscribe failed] (err %d)", err);
} else {
LOG_DBG("[SUBSCRIBED]");
}
} else if (!consumer_subscribe_params.ccc_handle) {
consumer_subscribe_params.notify = notify_func;
consumer_subscribe_params.value = BT_GATT_CCC_NOTIFY;
consumer_subscribe_params.ccc_handle = attr->handle;
err = bt_gatt_subscribe(conn, &consumer_subscribe_params);
if (err && err != -EALREADY) {
LOG_DBG("[Subscribe failed] (err %d)", err);
} else {
LOG_DBG("[CONSUMER SUBSCRIBED]");
}
} else {
LOG_DBG("[SUBSCRIBED]");
pointer_subscribe_params.notify = notify_func;
pointer_subscribe_params.value = BT_GATT_CCC_NOTIFY;
pointer_subscribe_params.ccc_handle = attr->handle;
err = bt_gatt_subscribe(conn, &pointer_subscribe_params);
if (err && err != -EALREADY) {
LOG_DBG("[Subscribe failed] (err %d)", err);
} else {
LOG_DBG("[MOUSE SUBSCRIBED]");
}
}
find_next_hids_report = write_hid_indicators_on_discovery;
find_next_hids_report = !consumer_subscribe_params.ccc_handle ||
write_hid_indicators_on_discovery || subscribe_to_pointer_report;
}
if (find_next_hids_report) {

View File

@ -23,9 +23,10 @@ profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 23
profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 28
profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 30
profile 1 <dbg> ble_central: discover_func: [SUBSCRIBED]
profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 32
profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 34
profile 1 <dbg> ble_central: discover_func: [CONSUMER SUBSCRIBED]
profile 1 <dbg> ble_central: notify_func: payload
profile 1 00 00 04 00 00 00 00 00 |........
profile 1 <dbg> ble_central: notify_func: payload
profile 1 00 00 00 00 00 00 00 00 |........
profile 1 <dbg> ble_central: notify_func: payload
profile 1 00 00 00 00 00 00 00 00 |........

View File

@ -23,9 +23,10 @@ profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 23
profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 28
profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 30
profile 1 <dbg> ble_central: discover_func: [SUBSCRIBED]
profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 32
profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 34
profile 1 <dbg> ble_central: discover_func: [CONSUMER SUBSCRIBED]
profile 1 <dbg> ble_central: notify_func: payload
profile 1 00 00 04 00 00 00 00 00 |........
profile 1 <dbg> ble_central: notify_func: payload
profile 1 00 00 00 00 00 00 00 00 |........
profile 1 <dbg> ble_central: notify_func: payload
profile 1 00 00 00 00 00 00 00 00 |........

View File

@ -13,6 +13,9 @@
<dbg> ble_central: discover_func: [ATTRIBUTE] handle 28
<dbg> ble_central: discover_func: [ATTRIBUTE] handle 30
<dbg> ble_central: discover_func: [SUBSCRIBED]
<dbg> ble_central: discover_func: [ATTRIBUTE] handle 32
<dbg> ble_central: discover_func: [ATTRIBUTE] handle 34
<dbg> ble_central: discover_func: [CONSUMER SUBSCRIBED]
<dbg> ble_central: notify_func: payload
00 00 04 00 00 00 00 00 |........
<dbg> ble_central: notify_func: payload
@ -21,5 +24,3 @@
00 00 05 00 00 00 00 00 |........
<dbg> ble_central: notify_func: payload
00 00 00 00 00 00 00 00 |........
<dbg> ble_central: notify_func: payload
00 00 00 00 00 00 00 00 |........

View File

@ -16,6 +16,9 @@ profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 23
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 28
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 30
profile 0 <dbg> ble_central: discover_func: [SUBSCRIBED]
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 32
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 34
profile 0 <dbg> ble_central: discover_func: [CONSUMER SUBSCRIBED]
profile 0 <dbg> ble_central: notify_func: payload
profile 0 00 00 04 00 00 00 00 00 |........
profile 0 <dbg> ble_central: notify_func: payload
@ -32,9 +35,10 @@ profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 23
profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 28
profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 30
profile 1 <dbg> ble_central: discover_func: [SUBSCRIBED]
profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 32
profile 1 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 34
profile 1 <dbg> ble_central: discover_func: [CONSUMER SUBSCRIBED]
profile 1 <dbg> ble_central: notify_func: payload
profile 1 00 00 05 00 00 00 00 00 |........
profile 1 <dbg> ble_central: notify_func: payload
profile 1 00 00 00 00 00 00 00 00 |........
profile 1 <dbg> ble_central: notify_func: payload
profile 1 00 00 00 00 00 00 00 00 |........

View File

@ -22,6 +22,9 @@
<dbg> ble_central: discover_func: [ATTRIBUTE] handle 28
<dbg> ble_central: discover_func: [ATTRIBUTE] handle 30
<dbg> ble_central: discover_func: [SUBSCRIBED]
<dbg> ble_central: discover_func: [ATTRIBUTE] handle 32
<dbg> ble_central: discover_func: [ATTRIBUTE] handle 34
<dbg> ble_central: discover_func: [CONSUMER SUBSCRIBED]
<dbg> ble_central: notify_func: payload
00 00 04 00 00 00 00 00 |........
<dbg> ble_central: notify_func: payload
@ -30,5 +33,3 @@
00 00 05 00 00 00 00 00 |........
<dbg> ble_central: notify_func: payload
00 00 00 00 00 00 00 00 |........
<dbg> ble_central: notify_func: payload
00 00 00 00 00 00 00 00 |........

View File

@ -21,6 +21,9 @@
<dbg> ble_central: discover_func: [ATTRIBUTE] handle 28
<dbg> ble_central: discover_func: [ATTRIBUTE] handle 30
<dbg> ble_central: discover_func: [SUBSCRIBED]
<dbg> ble_central: discover_func: [ATTRIBUTE] handle 32
<dbg> ble_central: discover_func: [ATTRIBUTE] handle 34
<dbg> ble_central: discover_func: [CONSUMER SUBSCRIBED]
<dbg> ble_central: notify_func: payload
00 00 04 00 00 00 00 00 |........
<dbg> ble_central: notify_func: payload
@ -29,5 +32,3 @@
00 00 05 00 00 00 00 00 |........
<dbg> ble_central: notify_func: payload
00 00 00 00 00 00 00 00 |........
<dbg> ble_central: notify_func: payload
00 00 00 00 00 00 00 00 |........

View File

@ -13,6 +13,9 @@ profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 23
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 28
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 30
profile 0 <dbg> ble_central: discover_func: [SUBSCRIBED]
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 32
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 34
profile 0 <dbg> ble_central: discover_func: [CONSUMER SUBSCRIBED]
profile 0 <dbg> ble_central: notify_func: payload
profile 0 00 00 04 00 00 00 00 00 |........
profile 0 <dbg> ble_central: notify_func: payload

View File

@ -13,6 +13,9 @@ profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 23
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 28
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 30
profile 0 <dbg> ble_central: discover_func: [SUBSCRIBED]
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 32
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 34
profile 0 <dbg> ble_central: discover_func: [CONSUMER SUBSCRIBED]
profile 0 <dbg> ble_central: notify_func: payload
profile 0 00 00 05 00 00 00 00 00 |........
profile 0 <dbg> ble_central: notify_func: payload

View File

@ -13,6 +13,9 @@ profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 23
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 28
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 30
profile 0 <dbg> ble_central: discover_func: [SUBSCRIBED]
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 32
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 34
profile 0 <dbg> ble_central: discover_func: [CONSUMER SUBSCRIBED]
profile 0 <dbg> ble_central: notify_func: payload
profile 0 00 00 04 00 00 00 00 00 |........
profile 0 <dbg> ble_central: notify_func: payload

View File

@ -0,0 +1 @@
s/^d_02: @[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9][0-9][0-9][0-9] .{19}/profile 0 /p

View File

@ -0,0 +1,2 @@
CONFIG_ZMK_SPLIT=y
CONFIG_ZMK_POINTING=y

View File

@ -0,0 +1,27 @@
#include <behaviors.dtsi>
#include <dt-bindings/zmk/bt.h>
#include <dt-bindings/zmk/keys.h>
#include "shared.dtsi"
&kscan {
/delete-property/ exit-after;
events = <>;
};
&split_listener {
status = "okay";
};
/ {
keymap {
compatible = "zmk,keymap";
default_layer {
bindings = <
&kp A &kp B
&bt BT_SEL 0 &bt BT_CLR>;
sensor-bindings = <&inc_dec_kp A B>;
};
};
};

View File

@ -0,0 +1,31 @@
#include <dt-bindings/zmk/kscan_mock.h>
#include <zephyr/dt-bindings/input/input-event-codes.h>
#include "shared.dtsi"
&kscan {
events = <>;
/delete-property/ exit-after;
};
/ {
mock_input: mock_input {
compatible = "zmk,input-mock";
status = "okay";
event-startup-delay = <4000>;
event-period = <2000>;
events
= <INPUT_EV_REL INPUT_REL_X 100 0>
, <INPUT_EV_REL INPUT_REL_Y 100 1>
, <INPUT_EV_REL INPUT_REL_X 40 0>
, <INPUT_EV_REL INPUT_REL_Y 50 1>
;
exit-after;
};
};
&split_input {
device = <&mock_input>;
};

View File

@ -0,0 +1,16 @@
/ {
splits {
#address-cells = <1>;
#size-cells = <0>;
split_input: split_input@0 {
compatible = "zmk,input-split";
reg = <0>;
};
};
split_listener: split_listener {
compatible = "zmk,input-listener";
status = "disabled";
device = <&split_input>;
};
};

View File

@ -0,0 +1,2 @@
./ble_test_central.exe -d=2 -subscribe_to_pointer_report
./tests_ble_split_peripheral-input_peripheral.exe -d=3

View File

@ -0,0 +1,25 @@
profile 0 <wrn> bt_id: No static addresses stored in controller
profile 0 <dbg> ble_central: main: [Bluetooth initialized]
profile 0 <dbg> ble_central: start_scan: [Scanning successfully started]
profile 0 <dbg> ble_central: device_found: [DEVICE]: FD:9E:B2:48:47:39 (random), AD evt type 0, AD data len 15, RSSI -56
profile 0 <dbg> ble_central: eir_found: [AD]: 25 data_len 2
profile 0 <dbg> ble_central: eir_found: [AD]: 1 data_len 1
profile 0 <dbg> ble_central: eir_found: [AD]: 2 data_len 4
profile 0 <dbg> ble_central: connected: [Connected]: FD:9E:B2:48:47:39 (random)
profile 0 <dbg> ble_central: connected: [Setting the security for the connection]
profile 0 <dbg> ble_central: pairing_complete: Pairing complete
profile 0 <dbg> ble_central: discover_conn: [Discovery started for conn]
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 23
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 28
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 30
profile 0 <dbg> ble_central: discover_func: [SUBSCRIBED]
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 32
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 34
profile 0 <dbg> ble_central: discover_func: [CONSUMER SUBSCRIBED]
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 36
profile 0 <dbg> ble_central: discover_func: [ATTRIBUTE] handle 38
profile 0 <dbg> ble_central: discover_func: [MOUSE SUBSCRIBED]
profile 0 <dbg> ble_central: notify_func: payload
profile 0 00 64 00 64 00 00 00 00 00 |.d.d.... .
profile 0 <dbg> ble_central: notify_func: payload
profile 0 00 28 00 32 00 00 00 00 00 |.(.2.... .