// SPDX-License-Identifier: GPL-2.0-or-later /* * BlueZ - Bluetooth protocol stack for Linux * * Copyright (C) 2014 Google Inc. * * */ #ifdef HAVE_CONFIG_H #include #endif #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include "lib/bluetooth.h" #include "lib/hci.h" #include "lib/hci_lib.h" #include "lib/l2cap.h" #include "lib/uuid.h" #include "src/shared/mainloop.h" #include "src/shared/util.h" #include "src/shared/att.h" #include "src/shared/queue.h" #include "src/shared/gatt-db.h" #include "src/shared/gatt-client.h" #define ATT_CID 4 #define PRLOG(...) \ printf(__VA_ARGS__); print_prompt(); #define COLOR_OFF "\x1B[0m" #define COLOR_RED "\x1B[0;91m" #define COLOR_GREEN "\x1B[0;92m" #define COLOR_YELLOW "\x1B[0;93m" #define COLOR_BLUE "\x1B[0;94m" #define COLOR_MAGENTA "\x1B[0;95m" #define COLOR_BOLDGRAY "\x1B[1;30m" #define COLOR_BOLDWHITE "\x1B[1;37m" static bool verbose = false; struct client { int fd; struct bt_att *att; struct gatt_db *db; struct bt_gatt_client *gatt; unsigned int reliable_session_id; }; static void print_prompt(void) { printf(COLOR_BLUE "[GATT client]" COLOR_OFF "# "); fflush(stdout); } static const char *ecode_to_string(uint8_t ecode) { switch (ecode) { case BT_ATT_ERROR_INVALID_HANDLE: return "Invalid Handle"; case BT_ATT_ERROR_READ_NOT_PERMITTED: return "Read Not Permitted"; case BT_ATT_ERROR_WRITE_NOT_PERMITTED: return "Write Not Permitted"; case BT_ATT_ERROR_INVALID_PDU: return "Invalid PDU"; case BT_ATT_ERROR_AUTHENTICATION: return "Authentication Required"; case BT_ATT_ERROR_REQUEST_NOT_SUPPORTED: return "Request Not Supported"; case BT_ATT_ERROR_INVALID_OFFSET: return "Invalid Offset"; case BT_ATT_ERROR_AUTHORIZATION: return "Authorization Required"; case BT_ATT_ERROR_PREPARE_QUEUE_FULL: return "Prepare Write Queue Full"; case BT_ATT_ERROR_ATTRIBUTE_NOT_FOUND: return "Attribute Not Found"; case BT_ATT_ERROR_ATTRIBUTE_NOT_LONG: return "Attribute Not Long"; case BT_ATT_ERROR_INSUFFICIENT_ENCRYPTION_KEY_SIZE: return "Insuficient Encryption Key Size"; case BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN: return "Invalid Attribute value len"; case BT_ATT_ERROR_UNLIKELY: return "Unlikely Error"; case BT_ATT_ERROR_INSUFFICIENT_ENCRYPTION: return "Insufficient Encryption"; case BT_ATT_ERROR_UNSUPPORTED_GROUP_TYPE: return "Group type Not Supported"; case BT_ATT_ERROR_INSUFFICIENT_RESOURCES: return "Insufficient Resources"; case BT_ERROR_CCC_IMPROPERLY_CONFIGURED: return "CCC Improperly Configured"; case BT_ERROR_ALREADY_IN_PROGRESS: return "Procedure Already in Progress"; case BT_ERROR_OUT_OF_RANGE: return "Out of Range"; default: return "Unknown error type"; } } static void att_disconnect_cb(int err, void *user_data) { printf("Device disconnected: %s\n", strerror(err)); mainloop_quit(); } static void att_debug_cb(const char *str, void *user_data) { const char *prefix = user_data; PRLOG(COLOR_BOLDGRAY "%s" COLOR_BOLDWHITE "%s\n" COLOR_OFF, prefix, str); } static void gatt_debug_cb(const char *str, void *user_data) { const char *prefix = user_data; PRLOG(COLOR_GREEN "%s%s\n" COLOR_OFF, prefix, str); } static void ready_cb(bool success, uint8_t att_ecode, void *user_data); static void service_changed_cb(uint16_t start_handle, uint16_t end_handle, void *user_data); static void log_service_event(struct gatt_db_attribute *attr, const char *str) { char uuid_str[MAX_LEN_UUID_STR]; bt_uuid_t uuid; uint16_t start, end; gatt_db_attribute_get_service_uuid(attr, &uuid); bt_uuid_to_string(&uuid, uuid_str, sizeof(uuid_str)); gatt_db_attribute_get_service_handles(attr, &start, &end); PRLOG("%s - UUID: %s start: 0x%04x end: 0x%04x\n", str, uuid_str, start, end); } static void service_added_cb(struct gatt_db_attribute *attr, void *user_data) { log_service_event(attr, "Service Added"); } static void service_removed_cb(struct gatt_db_attribute *attr, void *user_data) { log_service_event(attr, "Service Removed"); } static struct client *client_create(int fd, uint16_t mtu) { struct client *cli; cli = new0(struct client, 1); if (!cli) { fprintf(stderr, "Failed to allocate memory for client\n"); return NULL; } cli->att = bt_att_new(fd, false); if (!cli->att) { fprintf(stderr, "Failed to initialze ATT transport layer\n"); bt_att_unref(cli->att); free(cli); return NULL; } if (!bt_att_set_close_on_unref(cli->att, true)) { fprintf(stderr, "Failed to set up ATT transport layer\n"); bt_att_unref(cli->att); free(cli); return NULL; } if (!bt_att_register_disconnect(cli->att, att_disconnect_cb, NULL, NULL)) { fprintf(stderr, "Failed to set ATT disconnect handler\n"); bt_att_unref(cli->att); free(cli); return NULL; } cli->fd = fd; cli->db = gatt_db_new(); if (!cli->db) { fprintf(stderr, "Failed to create GATT database\n"); bt_att_unref(cli->att); free(cli); return NULL; } cli->gatt = bt_gatt_client_new(cli->db, cli->att, mtu, 0); if (!cli->gatt) { fprintf(stderr, "Failed to create GATT client\n"); gatt_db_unref(cli->db); bt_att_unref(cli->att); free(cli); return NULL; } gatt_db_register(cli->db, service_added_cb, service_removed_cb, NULL, NULL); if (verbose) { bt_att_set_debug(cli->att, BT_ATT_DEBUG_VERBOSE, att_debug_cb, "att: ", NULL); bt_gatt_client_set_debug(cli->gatt, gatt_debug_cb, "gatt: ", NULL); } bt_gatt_client_ready_register(cli->gatt, ready_cb, cli, NULL); bt_gatt_client_set_service_changed(cli->gatt, service_changed_cb, cli, NULL); /* bt_gatt_client already holds a reference */ gatt_db_unref(cli->db); return cli; } static void client_destroy(struct client *cli) { bt_gatt_client_unref(cli->gatt); bt_att_unref(cli->att); free(cli); } static void print_uuid(const bt_uuid_t *uuid) { char uuid_str[MAX_LEN_UUID_STR]; bt_uuid_t uuid128; bt_uuid_to_uuid128(uuid, &uuid128); bt_uuid_to_string(&uuid128, uuid_str, sizeof(uuid_str)); printf("%s\n", uuid_str); } static void print_incl(struct gatt_db_attribute *attr, void *user_data) { struct client *cli = user_data; uint16_t handle, start, end; struct gatt_db_attribute *service; bt_uuid_t uuid; if (!gatt_db_attribute_get_incl_data(attr, &handle, &start, &end)) return; service = gatt_db_get_attribute(cli->db, start); if (!service) return; gatt_db_attribute_get_service_uuid(service, &uuid); printf("\t " COLOR_GREEN "include" COLOR_OFF " - handle: " "0x%04x, - start: 0x%04x, end: 0x%04x," "uuid: ", handle, start, end); print_uuid(&uuid); } static void print_desc(struct gatt_db_attribute *attr, void *user_data) { printf("\t\t " COLOR_MAGENTA "descr" COLOR_OFF " - handle: 0x%04x, uuid: ", gatt_db_attribute_get_handle(attr)); print_uuid(gatt_db_attribute_get_type(attr)); } static void print_chrc(struct gatt_db_attribute *attr, void *user_data) { uint16_t handle, value_handle; uint8_t properties; uint16_t ext_prop; bt_uuid_t uuid; if (!gatt_db_attribute_get_char_data(attr, &handle, &value_handle, &properties, &ext_prop, &uuid)) return; printf("\t " COLOR_YELLOW "charac" COLOR_OFF " - start: 0x%04x, value: 0x%04x, " "props: 0x%02x, ext_props: 0x%04x, uuid: ", handle, value_handle, properties, ext_prop); print_uuid(&uuid); gatt_db_service_foreach_desc(attr, print_desc, NULL); } static void print_service(struct gatt_db_attribute *attr, void *user_data) { struct client *cli = user_data; uint16_t start, end; bool primary; bt_uuid_t uuid; if (!gatt_db_attribute_get_service_data(attr, &start, &end, &primary, &uuid)) return; printf(COLOR_RED "service" COLOR_OFF " - start: 0x%04x, " "end: 0x%04x, type: %s, uuid: ", start, end, primary ? "primary" : "secondary"); print_uuid(&uuid); gatt_db_service_foreach_incl(attr, print_incl, cli); gatt_db_service_foreach_char(attr, print_chrc, NULL); printf("\n"); } static void print_services(struct client *cli) { printf("\n"); gatt_db_foreach_service(cli->db, NULL, print_service, cli); } static void print_services_by_uuid(struct client *cli, const bt_uuid_t *uuid) { printf("\n"); gatt_db_foreach_service(cli->db, uuid, print_service, cli); } static void print_services_by_handle(struct client *cli, uint16_t handle) { printf("\n"); /* TODO: Filter by handle */ gatt_db_foreach_service(cli->db, NULL, print_service, cli); } static void ready_cb(bool success, uint8_t att_ecode, void *user_data) { struct client *cli = user_data; if (!success) { PRLOG("GATT discovery procedures failed - error code: 0x%02x\n", att_ecode); return; } PRLOG("GATT discovery procedures complete\n"); print_services(cli); print_prompt(); } static void service_changed_cb(uint16_t start_handle, uint16_t end_handle, void *user_data) { struct client *cli = user_data; printf("\nService Changed handled - start: 0x%04x end: 0x%04x\n", start_handle, end_handle); gatt_db_foreach_service_in_range(cli->db, NULL, print_service, cli, start_handle, end_handle); print_prompt(); } static void services_usage(void) { printf("Usage: services [options]\nOptions:\n" "\t -u, --uuid \tService UUID\n" "\t -a, --handle \tService start handle\n" "\t -h, --help\t\tShow help message\n" "e.g.:\n" "\tservices\n\tservices -u 0x180d\n\tservices -a 0x0009\n"); } static bool parse_args(char *str, int expected_argc, char **argv, int *argc) { char **ap; for (ap = argv; (*ap = strsep(&str, " \t")) != NULL;) { if (**ap == '\0') continue; (*argc)++; ap++; if (*argc > expected_argc) return false; } return true; } static void cmd_services(struct client *cli, char *cmd_str) { char *argv[3]; int argc = 0; if (!bt_gatt_client_is_ready(cli->gatt)) { printf("GATT client not initialized\n"); return; } if (!parse_args(cmd_str, 2, argv, &argc)) { services_usage(); return; } if (!argc) { print_services(cli); return; } if (argc != 2) { services_usage(); return; } if (!strcmp(argv[0], "-u") || !strcmp(argv[0], "--uuid")) { bt_uuid_t tmp, uuid; if (bt_string_to_uuid(&tmp, argv[1]) < 0) { printf("Invalid UUID: %s\n", argv[1]); return; } bt_uuid_to_uuid128(&tmp, &uuid); print_services_by_uuid(cli, &uuid); } else if (!strcmp(argv[0], "-a") || !strcmp(argv[0], "--handle")) { uint16_t handle; char *endptr = NULL; handle = strtol(argv[1], &endptr, 0); if (!endptr || *endptr != '\0') { printf("Invalid start handle: %s\n", argv[1]); return; } print_services_by_handle(cli, handle); } else services_usage(); } static void read_multiple_usage(void) { printf("Usage: read-multiple ...\n"); } static void read_multiple_cb(bool success, uint8_t att_ecode, const uint8_t *value, uint16_t length, void *user_data) { int i; if (!success) { PRLOG("\nRead multiple request failed: 0x%02x\n", att_ecode); return; } printf("\nRead multiple value (%u bytes):", length); for (i = 0; i < length; i++) printf("%02x ", value[i]); PRLOG("\n"); } static void cmd_read_multiple(struct client *cli, char *cmd_str) { int argc = 0; uint16_t *value; char *argv[512]; int i; char *endptr = NULL; if (!bt_gatt_client_is_ready(cli->gatt)) { printf("GATT client not initialized\n"); return; } if (!parse_args(cmd_str, sizeof(argv), argv, &argc) || argc < 2) { read_multiple_usage(); return; } value = malloc(sizeof(uint16_t) * argc); if (!value) { printf("Failed to construct value\n"); return; } for (i = 0; i < argc; i++) { value[i] = strtol(argv[i], &endptr, 0); if (endptr == argv[i] || *endptr != '\0' || !value[i]) { printf("Invalid value byte: %s\n", argv[i]); free(value); return; } } if (!bt_gatt_client_read_multiple(cli->gatt, value, argc, read_multiple_cb, NULL, NULL)) printf("Failed to initiate read multiple procedure\n"); free(value); } static void read_value_usage(void) { printf("Usage: read-value \n"); } static void read_cb(bool success, uint8_t att_ecode, const uint8_t *value, uint16_t length, void *user_data) { int i; if (!success) { PRLOG("\nRead request failed: %s (0x%02x)\n", ecode_to_string(att_ecode), att_ecode); return; } printf("\nRead value"); if (length == 0) { PRLOG(": 0 bytes\n"); return; } printf(" (%u bytes): ", length); for (i = 0; i < length; i++) printf("%02x ", value[i]); PRLOG("\n"); } static void cmd_read_value(struct client *cli, char *cmd_str) { char *argv[2]; int argc = 0; uint16_t handle; char *endptr = NULL; if (!bt_gatt_client_is_ready(cli->gatt)) { printf("GATT client not initialized\n"); return; } if (!parse_args(cmd_str, 1, argv, &argc) || argc != 1) { read_value_usage(); return; } handle = strtol(argv[0], &endptr, 0); if (!endptr || *endptr != '\0' || !handle) { printf("Invalid value handle: %s\n", argv[0]); return; } if (!bt_gatt_client_read_value(cli->gatt, handle, read_cb, NULL, NULL)) printf("Failed to initiate read value procedure\n"); } static void read_long_value_usage(void) { printf("Usage: read-long-value \n"); } static void cmd_read_long_value(struct client *cli, char *cmd_str) { char *argv[3]; int argc = 0; uint16_t handle; uint16_t offset; char *endptr = NULL; if (!bt_gatt_client_is_ready(cli->gatt)) { printf("GATT client not initialized\n"); return; } if (!parse_args(cmd_str, 2, argv, &argc) || argc != 2) { read_long_value_usage(); return; } handle = strtol(argv[0], &endptr, 0); if (!endptr || *endptr != '\0' || !handle) { printf("Invalid value handle: %s\n", argv[0]); return; } endptr = NULL; offset = strtol(argv[1], &endptr, 0); if (!endptr || *endptr != '\0') { printf("Invalid offset: %s\n", argv[1]); return; } if (!bt_gatt_client_read_long_value(cli->gatt, handle, offset, read_cb, NULL, NULL)) printf("Failed to initiate read long value procedure\n"); } static void write_value_usage(void) { printf("Usage: write-value [options] \n" "Options:\n" "\t-w, --without-response\tWrite without response\n" "\t-s, --signed-write\tSigned write command\n" "e.g.:\n" "\twrite-value 0x0001 00 01 00\n"); } static struct option write_value_options[] = { { "without-response", 0, 0, 'w' }, { "signed-write", 0, 0, 's' }, { } }; static void write_cb(bool success, uint8_t att_ecode, void *user_data) { if (success) { PRLOG("\nWrite successful\n"); } else { PRLOG("\nWrite failed: %s (0x%02x)\n", ecode_to_string(att_ecode), att_ecode); } } static void cmd_write_value(struct client *cli, char *cmd_str) { int opt, i, val; char *argvbuf[516]; char **argv = argvbuf; int argc = 1; uint16_t handle; char *endptr = NULL; int length; uint8_t *value = NULL; bool without_response = false; bool signed_write = false; if (!bt_gatt_client_is_ready(cli->gatt)) { printf("GATT client not initialized\n"); return; } if (!parse_args(cmd_str, 514, argv + 1, &argc)) { printf("Too many arguments\n"); write_value_usage(); return; } optind = 0; argv[0] = "write-value"; while ((opt = getopt_long(argc, argv, "+ws", write_value_options, NULL)) != -1) { switch (opt) { case 'w': without_response = true; break; case 's': signed_write = true; break; default: write_value_usage(); return; } } argc -= optind; argv += optind; if (argc < 1) { write_value_usage(); return; } handle = strtol(argv[0], &endptr, 0); if (!endptr || *endptr != '\0' || !handle) { printf("Invalid handle: %s\n", argv[0]); return; } length = argc - 1; if (length > 0) { if (length > UINT16_MAX) { printf("Write value too long\n"); return; } value = malloc(length); if (!value) { printf("Failed to construct write value\n"); return; } for (i = 1; i < argc; i++) { val = strtol(argv[i], &endptr, 0); if (endptr == argv[i] || *endptr != '\0' || errno == ERANGE || val < 0 || val > 255) { printf("Invalid value byte: %s\n", argv[i]); goto done; } value[i-1] = val; } } if (without_response) { if (!bt_gatt_client_write_without_response(cli->gatt, handle, signed_write, value, length)) { printf("Failed to initiate write without response " "procedure\n"); goto done; } printf("Write command sent\n"); goto done; } if (!bt_gatt_client_write_value(cli->gatt, handle, value, length, write_cb, NULL, NULL)) printf("Failed to initiate write procedure\n"); done: free(value); } static void write_long_value_usage(void) { printf("Usage: write-long-value [options] " "\n" "Options:\n" "\t-r, --reliable-write\tReliable write\n" "e.g.:\n" "\twrite-long-value 0x0001 0 00 01 00\n"); } static struct option write_long_value_options[] = { { "reliable-write", 0, 0, 'r' }, { } }; static void write_long_cb(bool success, bool reliable_error, uint8_t att_ecode, void *user_data) { if (success) { PRLOG("Write successful\n"); } else if (reliable_error) { PRLOG("Reliable write not verified\n"); } else { PRLOG("\nWrite failed: %s (0x%02x)\n", ecode_to_string(att_ecode), att_ecode); } } static void cmd_write_long_value(struct client *cli, char *cmd_str) { int opt, i, val; char *argvbuf[516]; char **argv = argvbuf; int argc = 1; uint16_t handle; uint16_t offset; char *endptr = NULL; int length; uint8_t *value = NULL; bool reliable_writes = false; if (!bt_gatt_client_is_ready(cli->gatt)) { printf("GATT client not initialized\n"); return; } if (!parse_args(cmd_str, 514, argv + 1, &argc)) { printf("Too many arguments\n"); write_value_usage(); return; } optind = 0; argv[0] = "write-long-value"; while ((opt = getopt_long(argc, argv, "+r", write_long_value_options, NULL)) != -1) { switch (opt) { case 'r': reliable_writes = true; break; default: write_long_value_usage(); return; } } argc -= optind; argv += optind; if (argc < 2) { write_long_value_usage(); return; } handle = strtol(argv[0], &endptr, 0); if (!endptr || *endptr != '\0' || !handle) { printf("Invalid handle: %s\n", argv[0]); return; } endptr = NULL; offset = strtol(argv[1], &endptr, 0); if (!endptr || *endptr != '\0' || errno == ERANGE) { printf("Invalid offset: %s\n", argv[1]); return; } length = argc - 2; if (length > 0) { if (length > UINT16_MAX) { printf("Write value too long\n"); return; } value = malloc(length); if (!value) { printf("Failed to construct write value\n"); return; } for (i = 2; i < argc; i++) { val = strtol(argv[i], &endptr, 0); if (endptr == argv[i] || *endptr != '\0' || errno == ERANGE || val < 0 || val > 255) { printf("Invalid value byte: %s\n", argv[i]); free(value); return; } value[i-2] = val; } } if (!bt_gatt_client_write_long_value(cli->gatt, reliable_writes, handle, offset, value, length, write_long_cb, NULL, NULL)) printf("Failed to initiate long write procedure\n"); free(value); } static void write_prepare_usage(void) { printf("Usage: write-prepare [options] " "\n" "Options:\n" "\t-s, --session-id\tSession id\n" "e.g.:\n" "\twrite-prepare -s 1 0x0001 00 01 00\n"); } static struct option write_prepare_options[] = { { "session-id", 1, 0, 's' }, { } }; static void cmd_write_prepare(struct client *cli, char *cmd_str) { int opt, i, val; char *argvbuf[516]; char **argv = argvbuf; int argc = 0; unsigned int id = 0; uint16_t handle; uint16_t offset; char *endptr = NULL; unsigned int length; uint8_t *value = NULL; if (!bt_gatt_client_is_ready(cli->gatt)) { printf("GATT client not initialized\n"); return; } if (!parse_args(cmd_str, 514, argv + 1, &argc)) { printf("Too many arguments\n"); write_value_usage(); return; } /* Add command name for getopt_long */ argc++; argv[0] = "write-prepare"; optind = 0; while ((opt = getopt_long(argc, argv , "s:", write_prepare_options, NULL)) != -1) { switch (opt) { case 's': if (!optarg) { write_prepare_usage(); return; } id = atoi(optarg); break; default: write_prepare_usage(); return; } } argc -= optind; argv += optind; if (argc < 3) { write_prepare_usage(); return; } if (cli->reliable_session_id != id) { printf("Session id != Ongoing session id (%u!=%u)\n", id, cli->reliable_session_id); return; } handle = strtol(argv[0], &endptr, 0); if (!endptr || *endptr != '\0' || !handle) { printf("Invalid handle: %s\n", argv[0]); return; } endptr = NULL; offset = strtol(argv[1], &endptr, 0); if (!endptr || *endptr != '\0' || errno == ERANGE) { printf("Invalid offset: %s\n", argv[1]); return; } /* * First two arguments are handle and offset. What remains is the value * length */ length = argc - 2; if (length == 0) goto done; if (length > UINT16_MAX) { printf("Write value too long\n"); return; } value = malloc(length); if (!value) { printf("Failed to allocate memory for value\n"); return; } for (i = 2; i < argc; i++) { val = strtol(argv[i], &endptr, 0); if (endptr == argv[i] || *endptr != '\0' || errno == ERANGE || val < 0 || val > 255) { printf("Invalid value byte: %s\n", argv[i]); free(value); return; } value[i-2] = val; } done: cli->reliable_session_id = bt_gatt_client_prepare_write(cli->gatt, id, handle, offset, value, length, write_long_cb, NULL, NULL); if (!cli->reliable_session_id) printf("Failed to proceed prepare write\n"); else printf("Prepare write success.\n" "Session id: %d to be used on next write\n", cli->reliable_session_id); free(value); } static void write_execute_usage(void) { printf("Usage: write-execute \n" "e.g.:\n" "\twrite-execute 1 0\n"); } static void cmd_write_execute(struct client *cli, char *cmd_str) { char *argvbuf[516]; char **argv = argvbuf; int argc = 0; char *endptr = NULL; unsigned int session_id; bool execute; if (!bt_gatt_client_is_ready(cli->gatt)) { printf("GATT client not initialized\n"); return; } if (!parse_args(cmd_str, 514, argv, &argc)) { printf("Too many arguments\n"); write_value_usage(); return; } if (argc < 2) { write_execute_usage(); return; } session_id = strtol(argv[0], &endptr, 0); if (!endptr || *endptr != '\0') { printf("Invalid session id: %s\n", argv[0]); return; } if (session_id != cli->reliable_session_id) { printf("Invalid session id: %u != %u\n", session_id, cli->reliable_session_id); return; } execute = !!strtol(argv[1], &endptr, 0); if (!endptr || *endptr != '\0') { printf("Invalid execute: %s\n", argv[1]); return; } if (execute) { if (!bt_gatt_client_write_execute(cli->gatt, session_id, write_cb, NULL, NULL)) printf("Failed to proceed write execute\n"); } else { bt_gatt_client_cancel(cli->gatt, session_id); } cli->reliable_session_id = 0; } static void register_notify_usage(void) { printf("Usage: register-notify \n"); } static void notify_cb(uint16_t value_handle, const uint8_t *value, uint16_t length, void *user_data) { int i; printf("\n\tHandle Value Not/Ind: 0x%04x - ", value_handle); if (length == 0) { PRLOG("(0 bytes)\n"); return; } printf("(%u bytes): ", length); for (i = 0; i < length; i++) printf("%02x ", value[i]); PRLOG("\n"); } static void register_notify_cb(uint16_t att_ecode, void *user_data) { if (att_ecode) { PRLOG("Failed to register notify handler " "- error code: 0x%02x\n", att_ecode); return; } PRLOG("Registered notify handler!\n"); } static void cmd_register_notify(struct client *cli, char *cmd_str) { char *argv[2]; int argc = 0; uint16_t value_handle; unsigned int id; char *endptr = NULL; if (!bt_gatt_client_is_ready(cli->gatt)) { printf("GATT client not initialized\n"); return; } if (!parse_args(cmd_str, 1, argv, &argc) || argc != 1) { register_notify_usage(); return; } value_handle = strtol(argv[0], &endptr, 0); if (!endptr || *endptr != '\0' || !value_handle) { printf("Invalid value handle: %s\n", argv[0]); return; } id = bt_gatt_client_register_notify(cli->gatt, value_handle, register_notify_cb, notify_cb, NULL, NULL); if (!id) { printf("Failed to register notify handler\n"); return; } printf("Registering notify handler with id: %u\n", id); } static void unregister_notify_usage(void) { printf("Usage: unregister-notify \n"); } static void cmd_unregister_notify(struct client *cli, char *cmd_str) { char *argv[2]; int argc = 0; unsigned int id; char *endptr = NULL; if (!bt_gatt_client_is_ready(cli->gatt)) { printf("GATT client not initialized\n"); return; } if (!parse_args(cmd_str, 1, argv, &argc) || argc != 1) { unregister_notify_usage(); return; } id = strtol(argv[0], &endptr, 0); if (!endptr || *endptr != '\0' || !id) { printf("Invalid notify id: %s\n", argv[0]); return; } if (!bt_gatt_client_unregister_notify(cli->gatt, id)) { printf("Failed to unregister notify handler with id: %u\n", id); return; } printf("Unregistered notify handler with id: %u\n", id); } static void set_security_usage(void) { printf("Usage: set-security \n" "level: 1-3\n" "e.g.:\n" "\tset-security 2\n"); } static void cmd_set_security(struct client *cli, char *cmd_str) { char *argv[2]; int argc = 0; char *endptr = NULL; int level; if (!bt_gatt_client_is_ready(cli->gatt)) { printf("GATT client not initialized\n"); return; } if (!parse_args(cmd_str, 1, argv, &argc)) { printf("Too many arguments\n"); set_security_usage(); return; } if (argc < 1) { set_security_usage(); return; } level = strtol(argv[0], &endptr, 0); if (!endptr || *endptr != '\0' || level < 1 || level > 3) { printf("Invalid level: %s\n", argv[0]); return; } if (!bt_gatt_client_set_security(cli->gatt, level)) printf("Could not set sec level\n"); else printf("Setting security level %d success\n", level); } static void cmd_get_security(struct client *cli, char *cmd_str) { int level; if (!bt_gatt_client_is_ready(cli->gatt)) { printf("GATT client not initialized\n"); return; } level = bt_gatt_client_get_security(cli->gatt); if (level < 0) printf("Could not set sec level\n"); else printf("Security level: %u\n", level); } static bool convert_sign_key(char *optarg, uint8_t key[16]) { int i; if (strlen(optarg) != 32) { printf("sign-key length is invalid\n"); return false; } for (i = 0; i < 16; i++) { if (sscanf(optarg + (i * 2), "%2hhx", &key[i]) != 1) return false; } return true; } static void set_sign_key_usage(void) { printf("Usage: set-sign-key [options]\nOptions:\n" "\t -c, --sign-key \tCSRK\n" "e.g.:\n" "\tset-sign-key -c D8515948451FEA320DC05A2E88308188\n"); } static bool local_counter(uint32_t *sign_cnt, void *user_data) { static uint32_t cnt = 0; *sign_cnt = cnt++; return true; } static void cmd_set_sign_key(struct client *cli, char *cmd_str) { char *argv[3]; int argc = 0; uint8_t key[16]; memset(key, 0, 16); if (!parse_args(cmd_str, 2, argv, &argc)) { set_sign_key_usage(); return; } if (argc != 2) { set_sign_key_usage(); return; } if (!strcmp(argv[0], "-c") || !strcmp(argv[0], "--sign-key")) { if (convert_sign_key(argv[1], key)) bt_att_set_local_key(cli->att, key, local_counter, cli); } else set_sign_key_usage(); } static void cmd_help(struct client *cli, char *cmd_str); typedef void (*command_func_t)(struct client *cli, char *cmd_str); static struct { char *cmd; command_func_t func; char *doc; } command[] = { { "help", cmd_help, "\tDisplay help message" }, { "services", cmd_services, "\tShow discovered services" }, { "read-value", cmd_read_value, "\tRead a characteristic or descriptor value" }, { "read-long-value", cmd_read_long_value, "\tRead a long characteristic or desctriptor value" }, { "read-multiple", cmd_read_multiple, "\tRead Multiple" }, { "write-value", cmd_write_value, "\tWrite a characteristic or descriptor value" }, { "write-long-value", cmd_write_long_value, "Write long characteristic or descriptor value" }, { "write-prepare", cmd_write_prepare, "\tWrite prepare characteristic or descriptor value" }, { "write-execute", cmd_write_execute, "\tExecute already prepared write" }, { "register-notify", cmd_register_notify, "\tSubscribe to not/ind from a characteristic" }, { "unregister-notify", cmd_unregister_notify, "Unregister a not/ind session"}, { "set-security", cmd_set_security, "\tSet security level on le connection"}, { "get-security", cmd_get_security, "\tGet security level on le connection"}, { "set-sign-key", cmd_set_sign_key, "\tSet signing key for signed write command"}, { } }; static void cmd_help(struct client *cli, char *cmd_str) { int i; printf("Commands:\n"); for (i = 0; command[i].cmd; i++) printf("\t%-15s\t%s\n", command[i].cmd, command[i].doc); } static void prompt_read_cb(int fd, uint32_t events, void *user_data) { ssize_t read; size_t len = 0; char *line = NULL; char *cmd = NULL, *args; struct client *cli = user_data; int i; if (events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) { mainloop_quit(); return; } read = getline(&line, &len, stdin); if (read < 0) { free(line); return; } if (read <= 1) { cmd_help(cli, NULL); print_prompt(); free(line); return; } line[read-1] = '\0'; args = line; while ((cmd = strsep(&args, " \t"))) if (*cmd != '\0') break; if (!cmd) goto failed; for (i = 0; command[i].cmd; i++) { if (strcmp(command[i].cmd, cmd) == 0) break; } if (command[i].cmd) command[i].func(cli, args); else fprintf(stderr, "Unknown command: %s\n", line); failed: print_prompt(); free(line); } static void signal_cb(int signum, void *user_data) { switch (signum) { case SIGINT: case SIGTERM: mainloop_quit(); break; default: break; } } static int l2cap_le_att_connect(bdaddr_t *src, bdaddr_t *dst, uint8_t dst_type, int sec) { int sock; struct sockaddr_l2 srcaddr, dstaddr; struct bt_security btsec; if (verbose) { char srcaddr_str[18], dstaddr_str[18]; ba2str(src, srcaddr_str); ba2str(dst, dstaddr_str); printf("btgatt-client: Opening L2CAP LE connection on ATT " "channel:\n\t src: %s\n\tdest: %s\n", srcaddr_str, dstaddr_str); } sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP); if (sock < 0) { perror("Failed to create L2CAP socket"); return -1; } /* Set up source address */ memset(&srcaddr, 0, sizeof(srcaddr)); srcaddr.l2_family = AF_BLUETOOTH; srcaddr.l2_cid = htobs(ATT_CID); srcaddr.l2_bdaddr_type = 0; bacpy(&srcaddr.l2_bdaddr, src); if (bind(sock, (struct sockaddr *)&srcaddr, sizeof(srcaddr)) < 0) { perror("Failed to bind L2CAP socket"); close(sock); return -1; } /* Set the security level */ memset(&btsec, 0, sizeof(btsec)); btsec.level = sec; if (setsockopt(sock, SOL_BLUETOOTH, BT_SECURITY, &btsec, sizeof(btsec)) != 0) { fprintf(stderr, "Failed to set L2CAP security level\n"); close(sock); return -1; } /* Set up destination address */ memset(&dstaddr, 0, sizeof(dstaddr)); dstaddr.l2_family = AF_BLUETOOTH; dstaddr.l2_cid = htobs(ATT_CID); dstaddr.l2_bdaddr_type = dst_type; bacpy(&dstaddr.l2_bdaddr, dst); printf("Connecting to device..."); fflush(stdout); if (connect(sock, (struct sockaddr *) &dstaddr, sizeof(dstaddr)) < 0) { perror(" Failed to connect"); close(sock); return -1; } printf(" Done\n"); return sock; } static void usage(void) { printf("btgatt-client\n"); printf("Usage:\n\tbtgatt-client [options]\n"); printf("Options:\n" "\t-i, --index \t\tSpecify adapter index, e.g. hci0\n" "\t-d, --dest \t\tSpecify the destination address\n" "\t-t, --type [random|public] \tSpecify the LE address type\n" "\t-m, --mtu \t\tThe ATT MTU to use\n" "\t-s, --security-level \tSet security level (low|medium|" "high|fips)\n" "\t-v, --verbose\t\t\tEnable extra logging\n" "\t-h, --help\t\t\tDisplay help\n"); } static struct option main_options[] = { { "index", 1, 0, 'i' }, { "dest", 1, 0, 'd' }, { "type", 1, 0, 't' }, { "mtu", 1, 0, 'm' }, { "security-level", 1, 0, 's' }, { "verbose", 0, 0, 'v' }, { "help", 0, 0, 'h' }, { } }; int main(int argc, char *argv[]) { int opt; int sec = BT_SECURITY_LOW; uint16_t mtu = 0; uint8_t dst_type = BDADDR_LE_PUBLIC; bool dst_addr_given = false; bdaddr_t src_addr, dst_addr; int dev_id = -1; int fd; struct client *cli; while ((opt = getopt_long(argc, argv, "+hvs:m:t:d:i:", main_options, NULL)) != -1) { switch (opt) { case 'h': usage(); return EXIT_SUCCESS; case 'v': verbose = true; break; case 's': if (strcmp(optarg, "low") == 0) sec = BT_SECURITY_LOW; else if (strcmp(optarg, "medium") == 0) sec = BT_SECURITY_MEDIUM; else if (strcmp(optarg, "high") == 0) sec = BT_SECURITY_HIGH; else if (strcmp(optarg, "fips") == 0) sec = BT_SECURITY_FIPS; else { fprintf(stderr, "Invalid security level\n"); return EXIT_FAILURE; } break; case 'm': { int arg; arg = atoi(optarg); if (arg <= 0) { fprintf(stderr, "Invalid MTU: %d\n", arg); return EXIT_FAILURE; } if (arg > UINT16_MAX) { fprintf(stderr, "MTU too large: %d\n", arg); return EXIT_FAILURE; } mtu = (uint16_t)arg; break; } case 't': if (strcmp(optarg, "random") == 0) dst_type = BDADDR_LE_RANDOM; else if (strcmp(optarg, "public") == 0) dst_type = BDADDR_LE_PUBLIC; else { fprintf(stderr, "Allowed types: random, public\n"); return EXIT_FAILURE; } break; case 'd': if (str2ba(optarg, &dst_addr) < 0) { fprintf(stderr, "Invalid remote address: %s\n", optarg); return EXIT_FAILURE; } dst_addr_given = true; break; case 'i': dev_id = hci_devid(optarg); if (dev_id < 0) { perror("Invalid adapter"); return EXIT_FAILURE; } break; default: fprintf(stderr, "Invalid option: %c\n", opt); return EXIT_FAILURE; } } if (!argc) { usage(); return EXIT_SUCCESS; } argc -= optind; argv += optind; optind = 0; if (argc) { usage(); return EXIT_SUCCESS; } if (dev_id == -1) bacpy(&src_addr, BDADDR_ANY); else if (hci_devba(dev_id, &src_addr) < 0) { perror("Adapter not available"); return EXIT_FAILURE; } if (!dst_addr_given) { fprintf(stderr, "Destination address required!\n"); return EXIT_FAILURE; } mainloop_init(); fd = l2cap_le_att_connect(&src_addr, &dst_addr, dst_type, sec); if (fd < 0) return EXIT_FAILURE; cli = client_create(fd, mtu); if (!cli) { close(fd); return EXIT_FAILURE; } if (mainloop_add_fd(fileno(stdin), EPOLLIN | EPOLLRDHUP | EPOLLHUP | EPOLLERR, prompt_read_cb, cli, NULL) < 0) { fprintf(stderr, "Failed to initialize console\n"); return EXIT_FAILURE; } print_prompt(); mainloop_run_with_signal(signal_cb, NULL); printf("\n\nShutting down...\n"); client_destroy(cli); return EXIT_SUCCESS; }