xref: /btstack/src/ble/gatt-service/hids_client.c (revision a381a464ca15126a7cbf15b070e35966cca429e8)
1 /*
2  * Copyright (C) 2021 BlueKitchen GmbH
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  * 3. Neither the name of the copyright holders nor the names of
14  *    contributors may be used to endorse or promote products derived
15  *    from this software without specific prior written permission.
16  * 4. Any redistribution, use, or modification is done solely for
17  *    personal benefit and not for any commercial purpose or for
18  *    monetary gain.
19  *
20  * THIS SOFTWARE IS PROVIDED BY BLUEKITCHEN GMBH AND CONTRIBUTORS
21  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MATTHIAS
24  * RINGWALD OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
27  * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
30  * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
31  * SUCH DAMAGE.
32  *
33  * Please inquire about commercial licensing options at
34  * [email protected]
35  *
36  */
37 
38 #define BTSTACK_FILE__ "hids_client.c"
39 
40 #include "btstack_config.h"
41 
42 #include <stdint.h>
43 #include <string.h>
44 
45 #include "ble/gatt-service/hids_client.h"
46 
47 #include "btstack_memory.h"
48 #include "ble/att_db.h"
49 #include "ble/core.h"
50 #include "ble/gatt_client.h"
51 #include "ble/sm.h"
52 #include "bluetooth_gatt.h"
53 #include "btstack_debug.h"
54 #include "btstack_event.h"
55 #include "btstack_run_loop.h"
56 #include "gap.h"
57 
58 static btstack_linked_list_t clients;
59 static uint16_t hids_cid_counter = 0;
60 
61 static void handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
62 
63 static uint16_t hids_get_next_cid(void){
64     if (hids_cid_counter == 0xffff) {
65         hids_cid_counter = 1;
66     } else {
67         hids_cid_counter++;
68     }
69     return hids_cid_counter;
70 }
71 
72 static uint8_t find_report_index_for_value_handle(hids_client_t * client, uint16_t value_handle){
73     uint8_t i;
74     for (i = 0; i < client->num_reports; i++){
75         if (client->reports[i].value_handle == value_handle){
76             return i;
77         }
78     }
79     return HIDS_CLIENT_INVALID_REPORT_INDEX;
80 }
81 
82 static uint8_t find_report_index_for_report_id_and_type(hids_client_t * client, uint8_t report_id, hid_report_type_t report_type){
83     uint8_t i;
84     for (i = 0; i < client->num_reports; i++){
85         if ( (client->reports[i].report_id == report_id) && (client->reports[i].report_type == report_type)){
86             return i;
87         }
88     }
89     return HIDS_CLIENT_INVALID_REPORT_INDEX;
90 }
91 
92 static hids_client_report_t * find_report_for_report_id_and_type(hids_client_t * client, uint8_t report_id, hid_report_type_t report_type){
93     uint8_t i;
94     for (i = 0; i < client->num_reports; i++){
95         if ( (client->reports[i].report_id == report_id) && (client->reports[i].report_type == report_type)){
96             return &client->reports[i];
97         }
98     }
99     return NULL;
100 }
101 
102 static hids_client_report_t * get_boot_mouse_input_report(hids_client_t * client){
103     return find_report_for_report_id_and_type(client, HID_BOOT_MODE_MOUSE_ID, HID_REPORT_TYPE_INPUT);
104 }
105 static hids_client_report_t * get_boot_keyboard_input_report(hids_client_t * client){
106     return find_report_for_report_id_and_type(client, HID_BOOT_MODE_KEYBOARD_ID, HID_REPORT_TYPE_INPUT);
107 }
108 
109 static void hids_client_add_characteristic(hids_client_t * client, gatt_client_characteristic_t * characteristic, uint8_t report_id, hid_report_type_t report_type){
110     uint8_t report_index = find_report_index_for_value_handle(client, characteristic->value_handle);
111     if (report_index != HIDS_CLIENT_INVALID_REPORT_INDEX){
112         return;
113     }
114 
115     if (client->active_report_index < HIDS_CLIENT_NUM_REPORTS) {
116         client->active_report_index++;
117         client->reports[client->active_report_index].value_handle = characteristic->value_handle;
118         client->reports[client->active_report_index].end_handle = characteristic->end_handle;
119         client->reports[client->active_report_index].properties = characteristic->properties;
120 
121         client->reports[client->active_report_index].report_id = report_id;
122         client->reports[client->active_report_index].report_type = report_type;
123         client->num_reports++;
124     } else {
125         log_info("not enough storage, increase HIDS_CLIENT_NUM_REPORTS");
126     }
127 }
128 
129 static uint8_t hids_client_get_characteristic(hids_client_t * client, uint8_t report_id, hid_report_type_t report_type, gatt_client_characteristic_t * characteristic){
130     uint8_t report_index = find_report_index_for_report_id_and_type(client, report_id, report_type);
131     if (report_index == HIDS_CLIENT_INVALID_REPORT_INDEX){
132         return report_index;
133     }
134 
135     characteristic->value_handle = client->reports[report_index].value_handle;
136     characteristic->end_handle = client->reports[report_index].end_handle;
137     characteristic->properties = client->reports[report_index].properties;
138     return report_index;
139 }
140 
141 
142 static hids_client_t * hids_create_client(hci_con_handle_t con_handle, uint16_t cid){
143     hids_client_t * client = btstack_memory_hids_client_get();
144     if (!client){
145         log_error("Not enough memory to create client");
146         return NULL;
147     }
148     client->state = HIDS_CLIENT_STATE_IDLE;
149     client->cid = cid;
150     client->con_handle = con_handle;
151 
152     btstack_linked_list_add(&clients, (btstack_linked_item_t *) client);
153     return client;
154 }
155 
156 static void hids_finalize_client(hids_client_t * client){
157     btstack_linked_list_remove(&clients, (btstack_linked_item_t *) client);
158     btstack_memory_hids_client_free(client);
159 }
160 
161 static hids_client_t * hids_get_client_for_con_handle(hci_con_handle_t con_handle){
162     btstack_linked_list_iterator_t it;
163     btstack_linked_list_iterator_init(&it, &clients);
164     while (btstack_linked_list_iterator_has_next(&it)){
165         hids_client_t * client = (hids_client_t *)btstack_linked_list_iterator_next(&it);
166         if (client->con_handle != con_handle) continue;
167         return client;
168     }
169     return NULL;
170 }
171 
172 static hids_client_t * hids_get_client_for_cid(uint16_t hids_cid){
173     btstack_linked_list_iterator_t it;
174     btstack_linked_list_iterator_init(&it, &clients);
175     while (btstack_linked_list_iterator_has_next(&it)){
176         hids_client_t * client = (hids_client_t *)btstack_linked_list_iterator_next(&it);
177         if (client->cid != hids_cid) continue;
178         return client;
179     }
180     return NULL;
181 }
182 
183 static void hids_emit_connection_established(hids_client_t * client, uint8_t status){
184     uint8_t event[8];
185     int pos = 0;
186     event[pos++] = HCI_EVENT_GATTSERVICE_META;
187     event[pos++] = sizeof(event) - 2;
188     event[pos++] = GATTSERVICE_SUBEVENT_HID_SERVICE_CONNECTED;
189     little_endian_store_16(event, pos, client->cid);
190     pos += 2;
191     event[pos++] = status;
192     event[pos++] = client->protocol_mode;
193     event[pos++] = client->num_instances;
194     (*client->client_handler)(HCI_EVENT_PACKET, 0, event, sizeof(event));
195 }
196 
197 
198 static void hids_run_for_client(hids_client_t * client){
199     uint8_t att_status;
200     gatt_client_service_t service;
201     gatt_client_characteristic_t characteristic;
202     uint8_t report_index;
203 
204     switch (client->state){
205         case HIDS_CLIENT_STATE_W2_QUERY_SERVICE:
206             client->state = HIDS_CLIENT_STATE_W4_SERVICE_RESULT;
207             att_status = gatt_client_discover_primary_services_by_uuid16(handle_gatt_client_event, client->con_handle, ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE);
208             // TODO handle status
209             UNUSED(att_status);
210             break;
211 
212         case HIDS_CLIENT_STATE_W2_QUERY_CHARACTERISTIC:
213             client->state = HIDS_CLIENT_STATE_W4_CHARACTERISTIC_RESULT;
214 
215             service.start_group_handle = client->services[client->service_index].start_handle;
216             service.end_group_handle = client->services[client->service_index].end_handle;
217             att_status = gatt_client_discover_characteristics_for_service(&handle_gatt_client_event, client->con_handle, &service);
218 
219             UNUSED(att_status);
220             break;
221 
222         case HIDS_CLIENT_STATE_W2_ENABLE_KEYBOARD:
223             client->state = HIDS_CLIENT_STATE_W4_KEYBOARD_ENABLED;
224 
225             report_index = hids_client_get_characteristic(client, HID_BOOT_MODE_KEYBOARD_ID, HID_REPORT_TYPE_INPUT, &characteristic);
226             if (report_index != HIDS_CLIENT_INVALID_REPORT_INDEX){
227                 att_status = gatt_client_write_client_characteristic_configuration(&handle_gatt_client_event, client->con_handle, &characteristic, GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION);
228             }
229 
230             if ((report_index == HIDS_CLIENT_INVALID_REPORT_INDEX) || (att_status != ERROR_CODE_SUCCESS)){
231                 client->reports[report_index].value_handle = 0;
232                 client->state = HIDS_CLIENT_STATE_CONNECTED;
233             }
234             break;
235 
236         case HIDS_CLIENT_STATE_W2_ENABLE_MOUSE:
237             client->state = HIDS_CLIENT_STATE_W4_MOUSE_ENABLED;
238 
239             report_index = hids_client_get_characteristic(client, HID_BOOT_MODE_MOUSE_ID, HID_REPORT_TYPE_INPUT, &characteristic);
240 
241             if (report_index != HIDS_CLIENT_INVALID_REPORT_INDEX){
242                 att_status = gatt_client_write_client_characteristic_configuration(&handle_gatt_client_event, client->con_handle, &characteristic, GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION);
243             }
244 
245             if ((report_index == HIDS_CLIENT_INVALID_REPORT_INDEX) || (att_status != ERROR_CODE_SUCCESS)){
246                 client->reports[report_index].value_handle = 0;
247                 client->state = HIDS_CLIENT_STATE_CONNECTED;
248             }
249 
250             break;
251 
252         case HIDS_CLIENT_STATE_W2_SET_PROTOCOL_MODE:
253             client->state = HIDS_CLIENT_STATE_W4_SET_PROTOCOL_MODE;
254             att_status = gatt_client_write_value_of_characteristic_without_response(client->con_handle, client->protocol_mode_value_handle, 1, (uint8_t *)&client->required_protocol_mode);
255             UNUSED(att_status);
256 
257             client->protocol_mode = client->required_protocol_mode;
258             client->state = HIDS_CLIENT_STATE_CONNECTED;
259             hids_emit_connection_established(client, ERROR_CODE_SUCCESS);
260             break;
261 
262         case HIDS_CLIENT_W2_SEND_REPORT:{
263             client->state = HIDS_CLIENT_STATE_CONNECTED;
264 
265             att_status = gatt_client_write_value_of_characteristic_without_response(client->con_handle,
266                 client->reports[client->active_report_index].value_handle,
267                 client->report_len, (uint8_t *)client->report);
268             UNUSED(att_status);
269             break;
270         }
271         default:
272             break;
273     }
274 }
275 
276 static void hids_client_setup_report_event(hids_client_t * client, uint8_t report_id, uint8_t *buffer, uint16_t report_len){
277     uint16_t pos = 0;
278     buffer[pos++] = HCI_EVENT_GATTSERVICE_META;
279     pos++;  // skip len
280     buffer[pos++] = GATTSERVICE_SUBEVENT_HID_REPORT;
281     little_endian_store_16(buffer, pos, client->cid);
282     pos += 2;
283     buffer[pos++] = report_id;
284     little_endian_store_16(buffer, pos, report_len);
285     pos += 2;
286     buffer[1] = pos + report_len - 2;
287 }
288 
289 static void handle_boot_keyboard_hid_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
290     UNUSED(packet_type);
291     UNUSED(channel);
292     UNUSED(size);
293 
294     if (hci_event_packet_get_type(packet) != GATT_EVENT_NOTIFICATION) return;
295 
296     hids_client_t * client = hids_get_client_for_con_handle(gatt_event_notification_get_handle(packet));
297     btstack_assert(client != NULL);
298 
299     uint8_t * in_place_event = packet;
300     hids_client_setup_report_event(client, HID_BOOT_MODE_KEYBOARD_ID, in_place_event, gatt_event_notification_get_value_length(packet));
301     (*client->client_handler)(HCI_EVENT_PACKET, client->cid, in_place_event, size);
302 }
303 
304 static void handle_boot_mouse_hid_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
305     UNUSED(packet_type);
306     UNUSED(channel);
307     UNUSED(size);
308 
309     if (hci_event_packet_get_type(packet) != GATT_EVENT_NOTIFICATION) return;
310 
311     hids_client_t * client = hids_get_client_for_con_handle(gatt_event_notification_get_handle(packet));
312     btstack_assert(client != NULL);
313 
314     uint8_t * in_place_event = packet;
315     hids_client_setup_report_event(client, HID_BOOT_MODE_MOUSE_ID, in_place_event, gatt_event_notification_get_value_length(packet));
316     (*client->client_handler)(HCI_EVENT_PACKET, client->cid, in_place_event, size);
317 }
318 
319 
320 static void handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
321     UNUSED(packet_type);
322     UNUSED(channel);
323     UNUSED(size);
324 
325     hids_client_t * client = NULL;
326     uint8_t att_status;
327     gatt_client_service_t service;
328     gatt_client_characteristic_t characteristic;
329 
330     hids_client_report_t * boot_keyboard_report;
331     hids_client_report_t * boot_mouse_report;
332 
333     switch(hci_event_packet_get_type(packet)){
334         case GATT_EVENT_SERVICE_QUERY_RESULT:
335             client = hids_get_client_for_con_handle(gatt_event_service_query_result_get_handle(packet));
336             btstack_assert(client != NULL);
337 
338             if (client->state != HIDS_CLIENT_STATE_W4_SERVICE_RESULT) {
339                 hids_emit_connection_established(client, GATT_CLIENT_IN_WRONG_STATE);
340                 hids_finalize_client(client);
341                 break;
342             }
343 
344             if (client->num_instances < MAX_NUM_HID_SERVICES){
345                 gatt_event_service_query_result_get_service(packet, &service);
346                 client->services[client->num_instances].start_handle = service.start_group_handle;
347                 client->services[client->num_instances].end_handle = service.end_group_handle;
348             }
349             client->num_instances++;
350             break;
351 
352         case GATT_EVENT_CHARACTERISTIC_QUERY_RESULT:
353             client = hids_get_client_for_con_handle(gatt_event_characteristic_query_result_get_handle(packet));
354             btstack_assert(client != NULL);
355 
356             gatt_event_characteristic_query_result_get_characteristic(packet, &characteristic);
357             switch (characteristic.uuid16){
358                 case ORG_BLUETOOTH_CHARACTERISTIC_BOOT_KEYBOARD_INPUT_REPORT:
359                     hids_client_add_characteristic(client, &characteristic, HID_BOOT_MODE_KEYBOARD_ID, HID_REPORT_TYPE_INPUT);
360                     break;
361 
362                 case ORG_BLUETOOTH_CHARACTERISTIC_BOOT_MOUSE_INPUT_REPORT:
363                     hids_client_add_characteristic(client, &characteristic, HID_BOOT_MODE_MOUSE_ID, HID_REPORT_TYPE_INPUT);
364                     break;
365 
366                 case ORG_BLUETOOTH_CHARACTERISTIC_PROTOCOL_MODE:
367                     client->protocol_mode_value_handle = characteristic.value_handle;
368                     break;
369 
370                 case ORG_BLUETOOTH_CHARACTERISTIC_BOOT_KEYBOARD_OUTPUT_REPORT:
371                     hids_client_add_characteristic(client, &characteristic, HID_BOOT_MODE_KEYBOARD_ID, HID_REPORT_TYPE_OUTPUT);
372                     break;
373 
374                 default:
375                     break;
376             }
377             break;
378 
379         case GATT_EVENT_QUERY_COMPLETE:
380             client = hids_get_client_for_con_handle(gatt_event_query_complete_get_handle(packet));
381             btstack_assert(client != NULL);
382 
383             att_status = gatt_event_query_complete_get_att_status(packet);
384 
385             switch (client->state){
386                 case HIDS_CLIENT_STATE_W4_SERVICE_RESULT:
387                     if (att_status != ATT_ERROR_SUCCESS){
388                         hids_emit_connection_established(client, att_status);
389                         hids_finalize_client(client);
390                         break;
391                     }
392 
393                     if (client->num_instances == 0){
394                         hids_emit_connection_established(client, ERROR_CODE_UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE);
395                         hids_finalize_client(client);
396                         break;
397                     }
398 
399                     if (client->num_instances > MAX_NUM_HID_SERVICES) {
400                         log_info("%d hid services found, only first %d can be stored, increase MAX_NUM_HID_SERVICES", client->num_instances, MAX_NUM_HID_SERVICES);
401                         client->num_instances = MAX_NUM_HID_SERVICES;
402                     }
403 
404                     client->state = HIDS_CLIENT_STATE_W2_QUERY_CHARACTERISTIC;
405                     client->service_index = 0;
406                     break;
407 
408                 case HIDS_CLIENT_STATE_W4_CHARACTERISTIC_RESULT:
409                     if (att_status != ATT_ERROR_SUCCESS){
410                         hids_emit_connection_established(client, att_status);
411                         hids_finalize_client(client);
412                         break;
413                     }
414 
415                     switch (client->required_protocol_mode){
416                         case HID_PROTOCOL_MODE_BOOT:
417                             if (get_boot_keyboard_input_report(client) != NULL){
418                                 client->state = HIDS_CLIENT_STATE_W2_ENABLE_KEYBOARD;
419                                 break;
420                             }
421                             if (get_boot_mouse_input_report(client) != NULL){
422                                 client->state = HIDS_CLIENT_STATE_W2_ENABLE_MOUSE;
423                                 break;
424                             }
425                             hids_emit_connection_established(client, ERROR_CODE_UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE);
426                             hids_finalize_client(client);
427                             break;
428                         default:
429                             break;
430                     }
431                     break;
432 
433                 case HIDS_CLIENT_STATE_W4_KEYBOARD_ENABLED:
434                     boot_keyboard_report = get_boot_keyboard_input_report(client);
435                     if (boot_keyboard_report != NULL){
436                         // setup listener
437                         characteristic.value_handle = boot_keyboard_report->value_handle;
438                         gatt_client_listen_for_characteristic_value_updates(&boot_keyboard_report->notifications, &handle_boot_keyboard_hid_event, client->con_handle, &characteristic);
439                     }
440 
441                     // check if there is mouse input report
442                     boot_mouse_report = get_boot_mouse_input_report(client);
443                     if (boot_mouse_report != NULL){
444                         client->state = HIDS_CLIENT_STATE_W2_ENABLE_MOUSE;
445                         break;
446                     }
447 
448                     // if none of the reports is found bail out
449                     if (!boot_mouse_report && !boot_keyboard_report){
450                         hids_emit_connection_established(client, ERROR_CODE_UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE);
451                         hids_finalize_client(client);
452                         break;
453                     }
454 
455                     // set protocol
456                     if (client->protocol_mode_value_handle != 0){
457                         client->state = HIDS_CLIENT_STATE_W2_SET_PROTOCOL_MODE;
458                     } else {
459                         client->protocol_mode = HID_PROTOCOL_MODE_BOOT;
460                         client->state = HIDS_CLIENT_STATE_CONNECTED;
461                         hids_emit_connection_established(client, ERROR_CODE_SUCCESS);
462                     }
463                     hids_run_for_client(client);
464                     break;
465 
466                 case HIDS_CLIENT_STATE_W4_MOUSE_ENABLED:
467                     boot_mouse_report = get_boot_mouse_input_report(client);
468                     if (boot_mouse_report != NULL){
469                         // setup listener
470                         characteristic.value_handle = boot_mouse_report->value_handle;
471                         gatt_client_listen_for_characteristic_value_updates(&boot_mouse_report->notifications, &handle_boot_mouse_hid_event, client->con_handle, &characteristic);
472                     }
473 
474                     boot_keyboard_report = get_boot_keyboard_input_report(client);
475                     if (!boot_mouse_report && !boot_keyboard_report){
476                         hids_emit_connection_established(client, ERROR_CODE_UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE);
477                         hids_finalize_client(client);
478                         break;
479                     }
480 
481                     if (client->protocol_mode_value_handle != 0){
482                         client->state = HIDS_CLIENT_STATE_W2_SET_PROTOCOL_MODE;
483                     } else {
484                         client->protocol_mode = HID_PROTOCOL_MODE_BOOT;
485                         client->state = HIDS_CLIENT_STATE_CONNECTED;
486                         hids_emit_connection_established(client, ERROR_CODE_SUCCESS);
487                     }
488                     break;
489                 default:
490                     break;
491             }
492             break;
493 
494         default:
495             break;
496     }
497 
498     if (client != NULL){
499         hids_run_for_client(client);
500     }
501 }
502 
503 uint8_t hids_client_connect(hci_con_handle_t con_handle, btstack_packet_handler_t packet_handler, hid_protocol_mode_t protocol_mode, uint16_t * hids_cid){
504     btstack_assert(packet_handler != NULL);
505 
506     hids_client_t * client = hids_get_client_for_con_handle(con_handle);
507     if (client != NULL){
508         return ERROR_CODE_COMMAND_DISALLOWED;
509     }
510 
511     uint16_t cid = hids_get_next_cid();
512     if (hids_cid != NULL) {
513         *hids_cid = cid;
514     }
515 
516     client = hids_create_client(con_handle, cid);
517     if (client == NULL) {
518         return BTSTACK_MEMORY_ALLOC_FAILED;
519     }
520 
521     client->required_protocol_mode = protocol_mode;
522     client->client_handler = packet_handler;
523     client->state = HIDS_CLIENT_STATE_W2_QUERY_SERVICE;
524 
525     hids_run_for_client(client);
526     return ERROR_CODE_SUCCESS;
527 }
528 
529 uint8_t hids_client_disconnect(uint16_t hids_cid){
530     hids_client_t * client = hids_get_client_for_cid(hids_cid);
531     if (client == NULL){
532         return ERROR_CODE_UNKNOWN_CONNECTION_IDENTIFIER;
533     }
534     // finalize connection
535     hids_finalize_client(client);
536     return ERROR_CODE_SUCCESS;
537 }
538 
539 uint8_t hids_client_send_report(uint16_t hids_cid, uint8_t report_id, const uint8_t * report, uint8_t report_len){
540     hids_client_t * client = hids_get_client_for_cid(hids_cid);
541     if (client == NULL){
542         return ERROR_CODE_UNKNOWN_CONNECTION_IDENTIFIER;
543     }
544 
545     if (client->state != HIDS_CLIENT_STATE_CONNECTED) {
546         return ERROR_CODE_COMMAND_DISALLOWED;
547     }
548 
549     uint8_t report_index = find_report_index_for_report_id_and_type(client, report_id, HID_REPORT_TYPE_OUTPUT);
550     if (report_index == HIDS_CLIENT_INVALID_REPORT_INDEX){
551         return ERROR_CODE_UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE;
552     }
553 
554     uint16_t mtu;
555     uint8_t status = gatt_client_get_mtu(client->con_handle, &mtu);
556 
557     if (status != ERROR_CODE_SUCCESS){
558         return status;
559     }
560 
561     if (mtu - 2 < report_len){
562         return ERROR_CODE_PARAMETER_OUT_OF_MANDATORY_RANGE;
563     }
564 
565     client->state = HIDS_CLIENT_W2_SEND_REPORT;
566     client->active_report_index = report_index;
567     client->report = report;
568     client->report_len = report_len;
569 
570     hids_run_for_client(client);
571     return ERROR_CODE_SUCCESS;
572 }
573 
574 void hids_client_init(void){}
575 
576 void hids_client_deinit(void){}
577