HID over GATT issue

Hello,

I would like some help in understanding how HID over GATT works in the nRF Connect SDK.

I'm digging into the peripheral_hid_mouse example, and I don't understand how to structure the data, for example mouse movement is sent like this:

uint8_t x_buff[2];
uint8_t y_buff[2];
uint8_t buffer[INPUT_REP_MOVEMENT_LEN];

int16_t x = MAX(MIN(x_delta, 0x07ff), -0x07ff);
int16_t y = MAX(MIN(y_delta, 0x07ff), -0x07ff);

/* Convert to little-endian. */
sys_put_le16(x, x_buff);
sys_put_le16(y, y_buff);

/* Encode report. */
BUILD_ASSERT(sizeof(buffer) == 3,
"Only 2 axis, 12-bit each, are supported");

buffer[0] = x_buff[0];
buffer[1] = (y_buff[0] << 4) | (x_buff[1] & 0x0f);
buffer[2] = (y_buff[1] << 4) | (y_buff[0] >> 4);


bt_hids_inp_rep_send(&hids_obj, conn_mode[i].conn,
INPUT_REP_MOVEMENT_INDEX,
buffer, sizeof(buffer), NULL);

My questions:

- Why are mouse movements converted to little endian? In the standard HID mouse example there is no need for this and the mouse works the same

- Why are the X/Y displacement values sent across three bytes? My understanding is that you either send 2 bytes as 8 bits per axis or 4 bytes as 16 bit per axis

After digging a bit I tried the following code with no success:

		// Send movement
		buffer[0] = x_buff[0];
		buffer[1] = (y_buff[0] << 4) | (x_buff[1] & 0x0f);
		buffer[2] = (y_buff[1] << 4) | (y_buff[0] >> 4);

		bt_hids_inp_rep_send(&hids_obj, conn_mode.conn,
							 INPUT_REP_MOVEMENT_INDEX,
							 buffer, sizeof(buffer), NULL);

		// Send a right click
		buffer[0] = 0b10000000;
		buffer[1] = 0x0;
		buffer[2] = 0x0;

		bt_hids_inp_rep_send(&hids_obj, conn_mode.conn,
							 INPUT_REP_BUTTONS_INDEX,
							 buffer, sizeof(buffer), NULL);

The general question here is, how do I properly send both buttons and movement data with bt_hids_inp_rep_send()?

Thanks

Parents
  • Hi Stefano

    - Why are mouse movements converted to little endian? In the standard HID mouse example there is no need for this and the mouse works the same

    The sys_put_le16(..) calls are essentially there to convert the 16-bit mouse coordinate variables to byte buffers for further processing. 

    It would be possible to skip this step, and simply use the x and y variables directly when assigning values to buffer[0,1,2], but I assume the developer found this method more intuitive. 

    Making an explicit conversion to little endian is also a way to ensure that this code would run the same whether you build for a little endian or big endian target. 

    - Why are the X/Y displacement values sent across three bytes? My understanding is that you either send 2 bytes as 8 bits per axis or 4 bytes as 16 bit per axis

    If you look at the mouse motion report you will notice that it defines the X and Y values as 12 bits each, which adds up to 3 bytes total:

    /* 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) */

    It should be possible to change this to 16 bits for each, in order to simplify the code, as long as you make sure to change the report from 3 to 4 bytes as well. You would also need to change the mouse_movement_send(..) function to account for this change. 

    The general question here is, how do I properly send both buttons and movement data with bt_hids_inp_rep_send()?

    Could you try set buffer[0] = 0b00000010 instead? 

    Otherwise it looks correct. 

    Best regards
    Torbjørn

  • If you look at the mouse motion report you will notice that it defines the X and Y values as 12 bits each, which adds up to 3 bytes total:

    Oh I see, but why is report size 2 if I'm sending 3 bytes?

    Anyway, I tried to reduce to 2 bytes for the movement and left report size to 2;

    		/* Report ID 2: Mouse motion */
    		0x85, 0x02,		  /* Report Id 2 */
    		0x09, 0x01,		  /* Usage (Pointer) */
    		0xA1, 0x00,		  /* Collection (Physical) */
    		0x75, 0x08, //0x0C/* Report Size (12) -> 8 */
    		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) */

    Then I send two movement bytes like this:

    uint8_t x_buff[2];
    uint8_t y_buff[2];
    
    int16_t x = MAX(MIN(25, 0x07ff), -0x07ff);
    int16_t y = MAX(MIN(25, 0x07ff), -0x07ff);
    
    // Convert to little-endian.
    sys_put_le16(x, x_buff);
    sys_put_le16(y, y_buff);
    
    uint8_t buffer[2] = {0x0, 0x0};
    buffer[0] = x_buff[0];
    buffer[1] = y_buff[0];
    
    bt_hids_inp_rep_send(&hids_obj, conn_mode.conn,
    					 INPUT_REP_MOVEMENT_INDEX,
    					 buffer, sizeof(buffer), NULL);

    This however does not seem to be right, because even though bytes get sent they are not received, the receiver (custom board) doesn't light up an led used to signal me that it has received HOGP data.

    I also tried button data alone with the following code, but only obtained some random scroll wheel movements:

    uint8_t buttons[3];
    buttons[0] = 0b00000010;
    buttons[1] = 0x0;
    buttons[2] = 0x0;
    
    bt_hids_inp_rep_send(&hids_obj, conn_mode.conn,
    					 INPUT_REP_BUTTONS_INDEX,
    					 buttons, sizeof(buttons), NULL);

    I think the key is to better understand the report descriptor, my understanding is that before each report (the buffer variable I'm sending) the report descriptor is sent to explain what's inside it. For mouse buttons + scroll wheel the example uses:

    		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) */

    inside of which I see four Report Size fields, doesn't this mean I should send 4 different informations?

    Thanks for your time

    SN

  • Hi

    StefanoN said:
    Oh I see, but why is report size 2 if I'm sending 3 bytes?

    Not sure what you mean. The report count should be 2, not the size. 

    To get the total size you need to multiply the count with the size:

    12 * 2 = 24 bits = 3 bytes

    StefanoN said:
    This however does not seem to be right, because even though bytes get sent they are not received, the receiver (custom board) doesn't light up an led used to signal me that it has received HOGP data.

    You verified that it worked earlier when you used the original report? 

    Possibly the issue is that the INPUT_REP_MOVEMENT_LEN definition is unchanged. You should set this to 2 if you have changed the size of the report. 

    Otherwise the size of the report will not match the size of the buffer you are sending, and the bt_hids_inp_rep_send(..) call will fail. 

    StefanoN said:
    I think the key is to better understand the report descriptor, my understanding is that before each report (the buffer variable I'm sending) the report descriptor is sent to explain what's inside it.

    The report descriptor is only exchanged once after your devices connect. 

    Originally HID devices where using USB only, and the report descriptors would be exchanged shortly after USB enumeration (when you physically connect the USB device to the host). Bluetooth HID is basically identical to USB HID, except that now the transport layer is Bluetooth instead of USB. 

    In this case the report descriptor is provided in a dedicated GATT characteristic, and this characteristic will be read once by the HID client after the connection is established. 

    StefanoN said:
    inside of which I see four Report Size fields, doesn't this mean I should send 4 different informations?

    Not quite. The way to read the button + scroll/pan report is that it starts with 5 one bit fields for Button1-5 (please note the typo on line 4 in your code snippet, the first report count is 5, not 3). 

    After that you have one 3 bit field to pad the rest of the byte (in order to make sure the following fields are byte aligned, simplifying the code further on). 

    After that you have one 8 bit field for the wheel and a second 8 bit field for the AC pan. 

    Please note that the report size on line 15 is reused by both the wheel and AC pan usages. For clarity I would have considered repeating Report Size (8) for both of these, but when the report size is unchanged you don't need to repeat it. 

    For more information about HID in general and HID reports specifically you might want to have a look here and here

    Best regards
    Torbjørn

  • Those resources were exactly what I need, now I understand the report descriptor much better.

    I tried to simplify everything by just sending the mouse buttons byte, so on the sending side I use this descriptor:

    static void hid_init(void)
    {
    	int err;
    	struct bt_hids_init_param hids_init_param = {0};
    	struct bt_hids_inp_rep *hids_inp_rep;
    	static const uint8_t mouse_movement_mask[DIV_ROUND_UP(INPUT_REP_MOVEMENT_LEN, 8)] = {0};
    
    	static const uint8_t report_map[] = {
    		0x05, 0x01, /* Usage Page (Generic Desktop) */
    		0x09, 0x02, /* Usage (Mouse) */
    		0xA1, 0x01, /* Collection (Application) */
    
    		/* Report ID 1: Mouse buttons + scroll wheel */
    		0x85, 0x01, /* Report Id 1 */
    
    		// Button data - 1 byte
    		0x05, 0x09, // Usage Page (Buttons)
    		0x09, 0x01, // Usage (Pointer)
    		0xA1, 0x00, // Collection (Physical)
    		0x95, 0x05, // Report Count (5) - 5 values
    		0x75, 0x01, // Report Size (1) - 1 bit each
    		0x19, 0x01, // Usage Minimum (1)
    		0x29, 0x05, // Usage Maximum (5)
    		0x15, 0x00, // Logical Minimum (0)
    		0x25, 0x01, // Logical Maximum (1)
    		0x81, 0x02, // Input (Data, Variable, Absolute) - HID spec Sec. 6.2.2.5
    
    		// Button data padding to complete the byte
    		0x95, 0x01, // Report Count (1) - 1 value
    		0x75, 0x03, // Report Size (3) - 3 bits long
    		0x81, 0x01, // Input (Constant) - for padding, can be ignored
    
    		0xC0, // End Collection (Physical)
    		0xC0, // End Collection (Application)
    
    
    	hids_init_param.rep_map.data = report_map;
    	hids_init_param.rep_map.size = sizeof(report_map);
    
    	hids_init_param.info.bcd_hid = BASE_USB_HID_SPEC_VERSION;
    	hids_init_param.info.b_country_code = 0x00;
    	hids_init_param.info.flags = (BT_HIDS_REMOTE_WAKE |
    								  BT_HIDS_NORMALLY_CONNECTABLE);
    
    	hids_inp_rep = &hids_init_param.inp_rep_group_init.reports[0];
    	hids_inp_rep->size = INPUT_REP_BUTTONS_LEN;
    	hids_inp_rep->id = INPUT_REP_REF_BUTTONS_ID;
    	hids_init_param.inp_rep_group_init.cnt++;
    
    	hids_init_param.is_mouse = true;
    	hids_init_param.pm_evt_handler = hids_pm_evt_handler;
    
    	err = bt_hids_init(&hids_obj, &hids_init_param);
    	__ASSERT(err == 0, "HIDS initialization failed\n");
    }

    Than I send the byte like this:

    uint8_t buttons[] = {0b00000010};
    
    while (1)
    {
    	k_sleep(K_SECONDS(1));
    	
    	bt_hids_inp_rep_send(&hids_obj, conn_mode.conn,
    						 1,
    						 buttons, sizeof(buttons), NULL);
    }

    This byte doesn't get received by the receiver, which is running the central_hids sample and receiving data with the hogp_notify_cb(), to which I only added a print of the data to a COM port, so that I read the received data while having the Jlink attached to the sender, see below:

    static uint8_t hogp_notify_cb(struct bt_hogp *hogp,
    							  struct bt_hogp_rep_info *rep,
    							  uint8_t err,
    
    							  const uint8_t *data)
    {
    
    	gpio_pin_toggle_dt(&led_batt_low);
    
    	uint8_t size = bt_hogp_rep_size(rep);
    	uint8_t i;
    
    	if (!data)
    	{
    		return BT_GATT_ITER_STOP;
    	}
    
    	// Send to COM port
    	char binaryStr[9] = {'\0'}; // 8 bits + 1 null terminator
    	for (i = 0; i < size; ++i)
    	{
    		byteToBinaryString(data[i], binaryStr);
    		uart_fifo_fill(dev, binaryStr, sizeof(binaryStr));
    		uart_fifo_fill(dev, "\n\r", 2);
    	}
    	uart_fifo_fill(dev, "========\n\r", 10);
    
    	return BT_GATT_ITER_CONTINUE;
    }

    I don't understand why this is not working, the only way I found to make it work is to un-comment the following piece of code from the hid_init() function and send exactly three bytes of data instead of 1, WITHOUT changing the report descriptor.

    hids_inp_rep++;
    hids_inp_rep->size = INPUT_REP_MOVEMENT_LEN;
    hids_inp_rep->id = INPUT_REP_REF_MOVEMENT_ID;
    hids_inp_rep->rep_mask = mouse_movement_mask;
    hids_init_param.inp_rep_group_init.cnt++;

    The only way I can explain this is that, from the wiki page you linked it says: "For example, a basic mouse defines a 3 byte Report...", which to my USB-new eye it seems to be enforced by this line in the hid_init() function:

    hids_init_param.is_mouse = true;

    Meaning that if I want a HID mouse I need at least 3 bytes, wether or not they are specified in the report descriptor.

    Seems a bit silly, what if my mouse only has buttons? This can't be right.

    Any ideas?

    Thanks

  • Solved.

    I had forgotten to modify INPUT_REP_BUTTONS_LEN, which was still set to 3, hence the code working only when 3 bytes where sent.

    After changing it to the correct value, 1 in this case, I was able to send a single byte of data successfully.

    Thanks for the help!

  • Glad to hear you found the issue.

    Do have a great weekend, and the best of luck with your project Slight smile

  • Hello,

    I'm facing the same issue you got resolved. I'm able to control the cursor movements but stuck in mouse button clicks.

    i'm calling this but nothing happens. Here, "INPUT_REP_BUTTONS_LEN" is set to "1".

    bt_hids_inp_rep_send(&hids_obj, conn_mode[i].conn,
    								1,
    								buffer, INPUT_REP_BUTTONS_LEN, NULL);

    Can you please share a snippet to simulate a right and left clicks of mouse?

    Thank you in advance.

    Regards,

Reply
  • Hello,

    I'm facing the same issue you got resolved. I'm able to control the cursor movements but stuck in mouse button clicks.

    i'm calling this but nothing happens. Here, "INPUT_REP_BUTTONS_LEN" is set to "1".

    bt_hids_inp_rep_send(&hids_obj, conn_mode[i].conn,
    								1,
    								buffer, INPUT_REP_BUTTONS_LEN, NULL);

    Can you please share a snippet to simulate a right and left clicks of mouse?

    Thank you in advance.

    Regards,

Children
Related