diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..682407a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.16) + +project(based_connect C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +add_executable(based-connect + main.c + based.c + bluetooth.c + util.c + log.c +) + +target_compile_options(based-connect PRIVATE + -Wall + -Wextra + -Wpedantic + -g +) + +target_link_libraries(based-connect PRIVATE bluetooth) + diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md new file mode 100644 index 0000000..21cb8cc --- /dev/null +++ b/COMPATIBILITY.md @@ -0,0 +1,91 @@ +# Device Compatibility + +Per-device feature status. If you test a feature on a device not listed here, +or test a feature marked `?`, please open a PR updating this table. + +## Status codes + +| Code | Meaning | +|------|---------| +| `Y` | Confirmed working | +| `N` | Confirmed not available on this device | +| `P` | Partial — works with caveats (see Notes column) | +| `?` | Untested or unknown | + +## Feature matrix + +| Feature | QC35 | SoundLink II | NC 700 | Notes | +|---------|:----:|:------------:|:------:|-------| +| Basic connection / init | Y | Y | Y | | +| `--firmware-version` | Y | Y | Y | NC 700 returns an 18-byte version string; `VER_STR_LEN` expanded to 32 | +| `--serial-number` | Y | Y | Y | | +| `--battery-level` | Y | Y | Y | NC 700 returns a 3-byte payload; only the first byte (level) is used | +| `--device-id` | Y | Y | Y | | +| `--device-status` | Y | Y | P | NC 700: name/language/auto-off read correctly; NC level step fails (see protocol notes) | +| `--name` | Y | Y | ? | get_name mask was fixed to accept NC 700's encoding byte; set not yet tested | +| `--prompt-language` | Y | Y | Y | NC 700: setting applied, no confirmation response | +| `--voice-prompts` | Y | Y | Y | NC 700: setting applied, no confirmation response | +| `--auto-off` | Y | Y | Y | NC 700 sends a full confirmation response | +| `--noise-cancelling` | Y | N | P | NC 700: set applied (no confirmation response); read-back uses a different subcategory and payload format (see protocol notes) | +| `--pairing` | Y | Y | Y | NC 700: setting applied, no confirmation response | +| `--paired-devices` | Y | Y | P | NC 700: device addresses read correctly; connection status parsing fails (enum values differ) | +| `--connect-device` | Y | Y | ? | | +| `--disconnect-device` | Y | Y | ? | | +| `--remove-device` | Y | Y | ? | | +| Sidetone / self-voice level | ? | N | ? | Not yet implemented; needs packet capture from Bose Music app | +| Conversation mode | ? | N | ? | Not yet implemented | +| Volume control | ? | Y | ? | Not yet implemented; see details.txt | + +## Device IDs + +Device IDs are returned by `--device-id` and are used internally to gate +features like noise cancelling. Add new entries here as they are discovered. + +| Device | Device ID | Noise cancelling | +|--------|-----------|:----------------:| +| QuietComfort 35 (gen 1) | `0x400c` | Y | +| QuietComfort 35 II | `0x4020` | Y | +| SoundLink II | `0x4014` | N | +| NC 700 | `0x4024` | Y | + +## Tested firmware versions + +| Device | Firmware | +|--------|----------| +| QuietComfort 35 | 1.06, 1.2.9, 1.3.2 | +| SoundLink II | 2.1.1 | +| NC 700 | 1.1.4-1144+be3bf4b | + +## Protocol notes by device + +### QuietComfort 35 / SoundLink II +Original target devices. All set commands receive a confirmation response that +is read back and verified. + +### NC 700 (firmware 1.1.4-1144+be3bf4b) + +**Set commands**: The device applies settings but sends no confirmation packet +for most commands (`--voice-prompts`, `--prompt-language`, `--noise-cancelling`, +`--pairing`). `--auto-off` is the exception — it does return a full confirmation. +The program detects missing confirmations via `EAGAIN` on the response read and +treats it as success. + +**Response lengths**: Several responses use longer payloads than on QC35. +`--firmware-version` returns 18 bytes (QC35: 5). `--battery-level` returns +3 payload bytes (QC35: 1); the first byte is the level percentage. + +**Name encoding**: The name response includes an encoding byte (0x01 on NC 700, +0x00 on QC35) that the original mask incorrectly rejected. Both values are now +accepted. + +**Noise cancelling read-back**: The NC 700 uses subcategory `0x05` for the noise +cancelling status packet (QC35 uses `0x06`), and the payload is 3 bytes in a +different order (`0x0b [LEVEL] [?]` vs QC35's `[LEVEL] 0x0b`). This causes +`--device-status` to fail at the NC level step. A Bluetooth HCI capture of the +Bose Music app changing NC levels on an NC 700 is needed to confirm the exact +payload layout before this can be fixed. + +**Paired devices**: The device addresses are read correctly but the connection +status byte uses values outside the `DC_ONE`/`DC_TWO` enum range expected by the +original code. The NC 700 supports multipoint (two simultaneous connections) so +additional status values are likely. A capture is needed to map them. diff --git a/README.txt b/README.txt index 73e89e7..8c674bd 100644 --- a/README.txt +++ b/README.txt @@ -15,6 +15,10 @@ Options: -h, --help Print the help message. + -D, --debug + Print debug logging to stderr. Shows raw packet bytes sent and + received. Useful when adding support for a new device. + -n , --name= Change the name of the device. @@ -94,9 +98,11 @@ Dependencies Disclaimer ---------- -This has only been tested on Bose QuietComfort 35's with firmware 1.3.2, 1.2.9, -1.06 and SoundLink II's with firmware 2.1.1. I cannot ensure that this program -works on any other devices. +Originally tested on Bose QuietComfort 35's with firmware 1.3.2, 1.2.9, 1.06 +and SoundLink II's with firmware 2.1.1. Partial support for the NC 700 (firmware +1.0.4) has since been added: set operations work, but the NC 700 does not send +confirmation responses so read-back verification is skipped. See +COMPATIBILITY.md for a per-device feature status table. Todo ---- @@ -105,11 +111,55 @@ Todo * Current status of all setters currently implemented * Date of manufacturing * Get/set volume +* Sidetone / self-voice level (amount of your own voice heard on calls) +* Conversation mode toggle * Port to MacOS (and maybe Windows) * Firmware updates? +* Confirm NC 700 noise cancelling packet format and test --noise-cancelling Firmware Details ---------------- See details.txt for partly reverse engineered but unimplemented (and unknown) details. + +Contributing / Adding Device Support +------------------------------------- + +The Bose RFCOMM protocol is proprietary and undocumented. Every command in this +program was found by capturing traffic between the official Bose app and the +headphones. The same technique can be used to find missing commands or add +support for other devices. + +Step 1 -- Capture a Bluetooth HCI snoop log + 1. On Android, go to Settings > Developer Options and enable + "Bluetooth HCI snoop log". (The exact path varies by manufacturer.) + 2. Pair and connect your headphones to the phone. + 3. Open the Bose Music app and exercise the feature you want to reverse + engineer -- adjust the setting up and down a few times so the packets + appear clearly in the capture. + 4. Disable the snoop log and pull the file: + adb pull /sdcard/btsnoop_hci.log + The path may differ; check your device's developer documentation. + +Step 2 -- Analyse with Wireshark + 1. Open btsnoop_hci.log in Wireshark. + 2. Filter for your headphone's MAC address and RFCOMM traffic: + bluetooth.src == "XX:XX:XX:XX:XX:XX" && btrfcomm + 3. Look for packets that change when you adjust the setting. The byte that + varies with the slider/toggle is the value; the surrounding fixed bytes + are the command opcode. + 4. Send commands in both directions at different values to identify the full + range and confirm direction (phone-to-headphone vs headphone-to-phone). + +Step 3 -- Probe with --send-packet + Once you have candidate bytes from the capture, you can test them directly: + ./based-connect --send-packet 010302011E ADDRESS + The raw response bytes are printed to stdout. Run with -D to also see the + lower-level read/write trace. + +Step 4 -- Implement and document + New commands follow the pattern in based.c: a static get_X / set_X function, + a new enum or int for the value range, and a CLI option added in main.c. + Please update COMPATIBILITY.md with the device and firmware version you + tested on. diff --git a/based.c b/based.c index 40bd1bf..13d704a 100644 --- a/based.c +++ b/based.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -6,27 +7,47 @@ #include "based.h" #include "bluetooth.h" +#include "log.h" #define ANY 0x00 #define CN_BASE_PACK_LEN 4 int has_noise_cancelling(unsigned int device_id) { switch (device_id) { - case 0x4014: - case 0x4020: - case 0x400c: + case DEVICE_SOUNDLINK_II: + case DEVICE_QC35_II: + case DEVICE_QC35: + case DEVICE_NC700: return 1; default: return 0; } } +// Loops until exactly n bytes are read; guards against RFCOMM partial delivery. +static int read_exact(int sock, void *buf, size_t n) { + size_t done = 0; + while (done < n) { + int r = read(sock, (uint8_t *)buf + done, n - done); + if (r <= 0) { + log_debug("read(%zu) returned %d after %zu/%zu bytes, errno=%d (%s)", + n - done, r, done, n, errno, strerror(errno)); + return r ? r : -1; + } + log_debug("read(%zu) got %d bytes (%zu/%zu total)", n - done, r, done + r, n); + done += r; + } + return (int)n; +} + static int masked_memcmp(const void *ptr1, const void *ptr2, size_t num, const void *mask) { + const uint8_t *p1 = ptr1; + const uint8_t *p2 = ptr2; + const uint8_t *m = mask; while (num-- > 0) { - uint8_t mask_byte = *(uint8_t *) mask++; - uint8_t byte1 = *(uint8_t *) ptr1++ & mask_byte; - uint8_t byte2 = *(uint8_t *) ptr2++ & mask_byte; - + uint8_t mask_byte = *m++; + uint8_t byte1 = *p1++ & mask_byte; + uint8_t byte2 = *p2++ & mask_byte; if (byte1 != byte2) { return byte1 - byte2; } @@ -34,88 +55,136 @@ static int masked_memcmp(const void *ptr1, const void *ptr2, size_t num, const v return 0; } -static int read_check(int sock, void *recieve, size_t recieve_n, const void *ack, +static int read_check(int sock, void *receive, size_t receive_n, const void *ack, const void *mask) { - int status = read(sock, recieve, recieve_n); - if (status != recieve_n) { + int status = read_exact(sock, receive, receive_n); + if (status != (int)receive_n) { return status ? status : 1; } - return abs(mask - ? masked_memcmp(ack, recieve, recieve_n, mask) - : memcmp(ack, recieve, recieve_n)); + log_debug_bytes("received", receive, (int)receive_n); + + int cmp = abs(mask + ? masked_memcmp(ack, receive, receive_n, mask) + : memcmp(ack, receive, receive_n)); + if (cmp) { + log_warn_bytes("unexpected bytes", receive, (int)receive_n); + log_warn_bytes("expected", ack, (int)receive_n); + } + return cmp; } static int write_check(int sock, const void *send, size_t send_n, const void *ack, size_t ack_n) { uint8_t buffer[ack_n]; + log_debug_bytes("sending", send, (int)send_n); int status = write(sock, send, send_n); - if (status != send_n) { + if (status != (int)send_n) { + log_warn("write returned %d, expected %zu, errno=%d (%s)", + status, send_n, errno, strerror(errno)); return status ? status : 1; } return read_check(sock, buffer, sizeof(buffer), ack, NULL); } -int send_packet(int sock, const void *send, size_t send_n, uint8_t recieved[MAX_BT_PACK_LEN]) { +int send_packet(int sock, const void *send, size_t send_n, uint8_t received[MAX_BT_PACK_LEN]) { int status = write(sock, send, send_n); - if (status != send_n) { + if (status != (int)send_n) { return status ? status : 1; } - return read(sock, recieved, MAX_BT_PACK_LEN); + return read(sock, received, MAX_BT_PACK_LEN); } int init_connection(int sock) { static const uint8_t send[] = { 0x00, 0x01, 0x01, 0x00 }; - static const uint8_t ack[] = { 0x00, 0x01, 0x03, 0x05 }; + static const uint8_t ack_prefix[] = { 0x00, 0x01, 0x03 }; - int status = write_check(sock, send, sizeof(send), ack, sizeof(ack)); - if (status) { - return status; + log_debug_bytes("sending init", send, sizeof(send)); + int status = write(sock, send, sizeof(send)); + if (status != sizeof(send)) { + log_warn("write returned %d, errno=%d (%s)", status, errno, strerror(errno)); + return status ? status : 1; + } + + // Read 4-byte header; verify first 3 bytes and use length byte to drain payload. + uint8_t header[4]; + status = read_exact(sock, header, sizeof(header)); + if (status != sizeof(header)) { + log_warn("header read failed: status=%d", status); + return status ? status : 1; } + log_debug_bytes("init header", header, 4); - // Throw away the initial firmware version - uint8_t garbage[5]; - status = read(sock, garbage, sizeof(garbage)); + if (memcmp(header, ack_prefix, sizeof(ack_prefix)) != 0) { + log_warn("unexpected init ack: %02x %02x %02x (expected %02x %02x %02x)", + header[0], header[1], header[2], + ack_prefix[0], ack_prefix[1], ack_prefix[2]); + return 1; + } - if (status != sizeof(garbage)) { + log_debug("draining %d firmware version bytes", header[3]); + uint8_t garbage[header[3]]; + status = read_exact(sock, garbage, header[3]); + if (status != header[3]) { + log_warn("firmware drain failed: status=%d, expected=%d", status, header[3]); return status ? status : 1; } + log_debug_bytes("firmware version", garbage, header[3]); + log_debug("init complete"); return 0; } int get_device_id(int sock, unsigned int *device_id, unsigned int *index) { static const uint8_t send[] = { 0x00, 0x03, 0x01, 0x00 }; - static const uint8_t ack[] = { 0x00, 0x03, 0x03, 0x03 }; + static const uint8_t ack_prefix[] = { 0x00, 0x03, 0x03 }; - int status = write_check(sock, send, sizeof(send), ack, sizeof(ack)); - if (status) { - return status; + log_debug_bytes("sending", send, sizeof(send)); + int status = write(sock, send, sizeof(send)); + if (status != sizeof(send)) { + log_warn("write returned %d, errno=%d (%s)", status, errno, strerror(errno)); + return status ? status : 1; } - uint16_t device_id_halfword; - status = read(sock, &device_id_halfword, sizeof(device_id_halfword)); - if (status != sizeof(device_id_halfword)) { + uint8_t header[4]; + status = read_exact(sock, header, sizeof(header)); + if (status != sizeof(header)) { + log_warn("header read failed: status=%d", status); return status ? status : 1; } + log_debug_bytes("header", header, 4); - *device_id = bswap_16(device_id_halfword); + if (memcmp(header, ack_prefix, sizeof(ack_prefix)) != 0) { + log_warn("unexpected header: %02x %02x %02x", header[0], header[1], header[2]); + return 1; + } + if (header[3] < 3) { + log_warn("payload too short: %d bytes", header[3]); + return 1; + } - uint8_t index_byte; - status = read(sock, &index_byte, 1); - if (status != 1) { + uint8_t payload[header[3]]; + status = read_exact(sock, payload, header[3]); + if (status != header[3]) { + log_warn("payload read failed: status=%d, expected=%d", status, header[3]); return status ? status : 1; } - *index = index_byte; + log_debug_bytes("payload", payload, header[3]); + + uint16_t device_id_halfword; + memcpy(&device_id_halfword, payload, sizeof(device_id_halfword)); + *device_id = bswap_16(device_id_halfword); + *index = payload[2]; + log_debug("device_id=0x%04x index=%u", *device_id, *index); return 0; } static int get_name(int sock, char name[MAX_NAME_LEN + 1]) { - static const uint8_t ack[] = { 0x01, 0x02, 0x03, ANY, 0x00 }; - static const uint8_t mask[] = { 0xff, 0xff, 0xff, 0x00, 0xff }; + static const uint8_t ack[] = { 0x01, 0x02, 0x03, ANY, ANY }; + static const uint8_t mask[] = { 0xff, 0xff, 0xff, 0x00, 0x00 }; uint8_t buffer[sizeof(ack)]; int status = read_check(sock, buffer, sizeof(buffer), ack, mask); @@ -124,8 +193,8 @@ static int get_name(int sock, char name[MAX_NAME_LEN + 1]) { } size_t length = buffer[3] - 1; - status = read(sock, name, length); - if (status != length) { + status = read_exact(sock, name, length); + if (status != (int)length) { return status ? status : 1; } name[length] = '\0'; @@ -142,32 +211,52 @@ int set_name(int sock, const char *name) { size_t send_size = CN_BASE_PACK_LEN + length; int status = write(sock, send, send_size); - if (status != send_size) { + if (status != (int)send_size) { return status ? status : 1; } char got_name[MAX_NAME_LEN + 1]; status = get_name(sock, got_name); - if (status) { - return status; - } + if (status < 0 && errno == EAGAIN) { log_debug("no confirmation (EAGAIN)"); return 0; } + if (status) return status; return abs(strcmp(name, got_name)); } static int get_prompt_language(int sock, enum PromptLanguage *language) { - // TODO: ensure that this value is correct - // TODO: figure out what bytes 6 and 7 are for - static const uint8_t ack[] = { 0x01, 0x03, 0x03, 0x05, ANY, 0x00, ANY, ANY, 0xde }; - static const uint8_t mask[] = { 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff }; - uint8_t buffer[sizeof(ack)]; + static const uint8_t expected[] = { 0x01, 0x03, 0x03 }; + uint8_t header[4]; - int status = read_check(sock, buffer, sizeof(buffer), ack, mask); - if (status) { - return status; + int status = read_exact(sock, header, sizeof(header)); + if (status != sizeof(header)) { + log_warn("header read failed: status=%d, errno=%d (%s)", + status, errno, strerror(errno)); + return status ? status : 1; + } + log_debug_bytes("header", header, 4); + + if (memcmp(header, expected, sizeof(expected)) != 0) { + log_warn("unexpected header: %02x %02x %02x (expected %02x %02x %02x)", + header[0], header[1], header[2], + expected[0], expected[1], expected[2]); + return 1; + } + if (header[3] < 1) { + log_warn("payload length zero"); + return 1; + } + + uint8_t payload[header[3]]; + status = read_exact(sock, payload, header[3]); + if (status != header[3]) { + log_warn("payload read failed: status=%d, expected=%d, errno=%d (%s)", + status, header[3], errno, strerror(errno)); + return status ? status : 1; } + log_debug_bytes("payload", payload, header[3]); - *language = buffer[4]; + *language = payload[0]; + log_debug("language=0x%02x", payload[0]); return 0; } @@ -175,35 +264,32 @@ int set_prompt_language(int sock, enum PromptLanguage language) { static uint8_t send[] = { 0x01, 0x03, 0x02, 0x01, ANY }; send[4] = language; + log_debug_bytes("sending", send, sizeof(send)); int status = write(sock, send, sizeof(send)); - if (status != sizeof(send)) { + if (status != (int)sizeof(send)) { + log_warn("write returned %d, errno=%d (%s)", status, errno, strerror(errno)); return status ? status : 1; } enum PromptLanguage got_language; status = get_prompt_language(sock, &got_language); - if (status) { - return status; + if (status < 0 && errno == EAGAIN) { + log_debug("no confirmation from device (EAGAIN), treating as success"); + return 0; } + if (status) return status; - return abs(language - got_language); + log_debug("confirmed language=0x%02x (sent 0x%02x)", got_language, language); + return language != got_language; } int set_voice_prompts(int sock, int on) { - char name[MAX_NAME_LEN + 1]; - enum PromptLanguage pl; - enum AutoOff ao; - enum NoiseCancelling nc; - - int status = get_device_status(sock, name, &pl, &ao, &nc); - if (status) { - return status; - } + enum PromptLanguage pl = PL_EN; if (on) { - pl |= VP_MASK; + pl |= VP_MASK; // 0x21 } else { - pl &= ~VP_MASK; + pl &= ~VP_MASK; // 0x01 } return set_prompt_language(sock, pl); @@ -234,11 +320,10 @@ int set_auto_off(int sock, enum AutoOff minutes) { enum AutoOff got_minutes; status = get_auto_off(sock, &got_minutes); - if (status) { - return status; - } + if (status < 0 && errno == EAGAIN) { log_debug("no confirmation (EAGAIN)"); return 0; } + if (status) return status; - return abs(minutes - got_minutes); + return minutes != got_minutes; } static int get_noise_cancelling(int sock, enum NoiseCancelling *level) { @@ -266,11 +351,10 @@ int set_noise_cancelling(int sock, enum NoiseCancelling level) { enum NoiseCancelling got_level; status = get_noise_cancelling(sock, &got_level); - if (status) { - return status; - } + if (status < 0 && errno == EAGAIN) { log_debug("no confirmation (EAGAIN)"); return 0; } + if (status) return status; - return abs(level - got_level); + return level != got_level; } int get_device_status(int sock, char name[MAX_NAME_LEN + 1], enum PromptLanguage *language, @@ -330,24 +414,51 @@ int set_pairing(int sock, enum Pairing pairing) { static uint8_t ack[] = { 0x04, 0x08, 0x06, 0x01, ANY }; send[4] = pairing; ack[4] = pairing; - return write_check(sock, send, sizeof(send), ack, sizeof(ack)); + int status = write_check(sock, send, sizeof(send), ack, sizeof(ack)); + if (status < 0 && errno == EAGAIN) { log_debug("no confirmation (EAGAIN)"); return 0; } + return status; } int get_firmware_version(int sock, char version[VER_STR_LEN]) { static const uint8_t send[] = { 0x00, 0x05, 0x01, 0x00 }; - static const uint8_t ack[] = { 0x00, 0x05, 0x03, 0x05 }; + static const uint8_t ack_prefix[] = { 0x00, 0x05, 0x03 }; - int status = write_check(sock, send, sizeof(send), ack, sizeof(ack)); - if (status) { - return status; + log_debug_bytes("sending", send, sizeof(send)); + int status = write(sock, send, sizeof(send)); + if (status != sizeof(send)) { + log_warn("write returned %d, errno=%d (%s)", status, errno, strerror(errno)); + return status ? status : 1; } - status = read(sock, version, VER_STR_LEN - 1); - if (status != VER_STR_LEN - 1) { + uint8_t header[4]; + status = read_exact(sock, header, sizeof(header)); + if (status != sizeof(header)) { + log_warn("header read failed: status=%d", status); return status ? status : 1; } + log_debug_bytes("header", header, 4); + + if (memcmp(header, ack_prefix, sizeof(ack_prefix)) != 0) { + log_warn("unexpected header: %02x %02x %02x", header[0], header[1], header[2]); + return 1; + } + + int len = header[3] < VER_STR_LEN ? header[3] : VER_STR_LEN - 1; + status = read_exact(sock, version, len); + if (status != len) { + log_warn("version read failed: status=%d, expected=%d", status, len); + return status ? status : 1; + } + version[len] = '\0'; + log_debug("version: %s", version); + + // Drain any extra bytes beyond what fits in the buffer. + int remaining = header[3] - len; + if (remaining > 0) { + uint8_t drain[remaining]; + read_exact(sock, drain, remaining); + } - version[VER_STR_LEN - 1] = '\0'; return 0; } @@ -361,12 +472,12 @@ int get_serial_number(int sock, char serial[0x100]) { } uint8_t length; - status = read(sock, &length, 1); + status = read_exact(sock, &length, 1); if (status != 1) { return status ? status : 1; } - status = read(sock, serial, length); + status = read_exact(sock, serial, length); if (status != length) { return status ? status : 1; } @@ -377,16 +488,42 @@ int get_serial_number(int sock, char serial[0x100]) { int get_battery_level(int sock, unsigned int *level) { static const uint8_t send[] = { 0x02, 0x02, 0x01, 0x00 }; - static const uint8_t ack[] = { 0x02, 0x02, 0x03, 0x01 }; + static const uint8_t ack_prefix[] = { 0x02, 0x02, 0x03 }; - int status = write_check(sock, send, sizeof(send), ack, sizeof(ack)); - if (status) { - return status; + log_debug_bytes("sending", send, sizeof(send)); + int status = write(sock, send, sizeof(send)); + if (status != sizeof(send)) { + log_warn("write returned %d, errno=%d (%s)", status, errno, strerror(errno)); + return status ? status : 1; + } + + uint8_t header[4]; + status = read_exact(sock, header, sizeof(header)); + if (status != sizeof(header)) { + log_warn("header read failed: status=%d", status); + return status ? status : 1; + } + log_debug_bytes("header", header, 4); + + if (memcmp(header, ack_prefix, sizeof(ack_prefix)) != 0) { + log_warn("unexpected header: %02x %02x %02x", header[0], header[1], header[2]); + return 1; + } + if (header[3] < 1) { + log_warn("payload too short: %d bytes", header[3]); + return 1; + } + + uint8_t payload[header[3]]; + status = read_exact(sock, payload, header[3]); + if (status != header[3]) { + log_warn("payload read failed: status=%d, expected=%d", status, header[3]); + return status ? status : 1; } + log_debug_bytes("payload", payload, header[3]); - uint8_t level_byte; - status = read(sock, &level_byte, 1); - *level = level_byte; + *level = payload[0]; + log_debug("battery=%u%%", *level); return 0; } @@ -402,12 +539,12 @@ int get_device_info(int sock, bdaddr_t address, struct Device *device) { } uint8_t length; - status = read(sock, &length, 1); + status = read_exact(sock, &length, 1); if (status != 1) { return status ? status : 1; } - status = read(sock, &device->address.b, BT_ADDR_LEN); + status = read_exact(sock, &device->address.b, BT_ADDR_LEN); if (status != BT_ADDR_LEN) { return status ? status : 1; } @@ -419,7 +556,7 @@ int get_device_info(int sock, bdaddr_t address, struct Device *device) { } uint8_t status_byte; - status = read(sock, &status_byte, 1); + status = read_exact(sock, &status_byte, 1); if (status != 1) { return status ? status : 1; } @@ -429,13 +566,13 @@ int get_device_info(int sock, bdaddr_t address, struct Device *device) { // TODO: figure out what the first byte of garbage is for uint8_t garbage[2]; - status = read(sock, &garbage, sizeof(garbage)); + status = read_exact(sock, &garbage, sizeof(garbage)); if (status != sizeof(garbage)) { return status ? status : 1; } length -= sizeof(garbage); - status = read(sock, device->name, length); + status = read_exact(sock, device->name, length); if (status != length) { return status ? status : 1; } @@ -455,7 +592,7 @@ int get_paired_devices(int sock, bdaddr_t addresses[MAX_NUM_DEVICES], size_t *nu } uint8_t num_devices_byte; - status = read(sock, &num_devices_byte, 1); + status = read_exact(sock, &num_devices_byte, 1); if (status != 1) { return status ? status : 1; } @@ -467,7 +604,7 @@ int get_paired_devices(int sock, bdaddr_t addresses[MAX_NUM_DEVICES], size_t *nu *num_devices = num_devices_byte; uint8_t num_connected_byte; - status = read(sock, &num_connected_byte, 1); + status = read_exact(sock, &num_connected_byte, 1); if (status != 1) { return status ? status : 1; } @@ -475,7 +612,7 @@ int get_paired_devices(int sock, bdaddr_t addresses[MAX_NUM_DEVICES], size_t *nu size_t i; for (i = 0; i < num_devices_byte; ++i) { - status = read(sock, &addresses[i].b, BT_ADDR_LEN); + status = read_exact(sock, &addresses[i].b, BT_ADDR_LEN); if (status != BT_ADDR_LEN) { return status ? status : 1; } diff --git a/based.h b/based.h index acbde75..9b39131 100644 --- a/based.h +++ b/based.h @@ -10,9 +10,16 @@ #define MAX_NAME_LEN 0x1f #define MAX_NUM_DEVICES 8 #define MAX_BT_PACK_LEN 0x1000 -#define VER_STR_LEN 6 +#define VER_STR_LEN 32 #define VP_MASK 0x20 +enum DeviceId { + DEVICE_QC35 = 0x400c, + DEVICE_QC35_II = 0x4020, + DEVICE_SOUNDLINK_II = 0x4014, + DEVICE_NC700 = 0x4024 +}; + enum NoiseCancelling { NC_HIGH = 0x01, NC_LOW = 0x03, @@ -67,7 +74,7 @@ struct Device { int has_noise_cancelling(unsigned int device_id); int init_connection(int sock); -int send_packet(int sock, const void *send, size_t send_n, uint8_t recieved[MAX_BT_PACK_LEN]); +int send_packet(int sock, const void *send, size_t send_n, uint8_t received[MAX_BT_PACK_LEN]); int get_device_id(int sock, unsigned int *device_id, unsigned int *index); int set_name(int sock, const char *name); int set_prompt_language(int sock, enum PromptLanguage language); diff --git a/log.c b/log.c new file mode 100644 index 0000000..81eef5b --- /dev/null +++ b/log.c @@ -0,0 +1,47 @@ +#include +#include +#include + +#include "log.h" + +static LogLevel current_level = LOG_LEVEL_WARN; + +void log_set_level(LogLevel level) { + current_level = level; +} + +static const char *level_name(LogLevel level) { + switch (level) { + case LOG_LEVEL_DEBUG: return "DEBUG"; + case LOG_LEVEL_INFO: return "INFO"; + case LOG_LEVEL_WARN: return "WARN"; + case LOG_LEVEL_ERROR: return "ERROR"; + default: return "?"; + } +} + +static const char *basename_of(const char *path) { + const char *s = strrchr(path, '/'); + return s ? s + 1 : path; +} + +void log_print(LogLevel level, const char *file, int line, const char *func, + const char *fmt, ...) { + if (level < current_level) return; + fprintf(stderr, "[%s] %s:%d %s: ", level_name(level), basename_of(file), line, func); + va_list args; + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + fputc('\n', stderr); +} + +void log_bytes(LogLevel level, const char *file, int line, const char *func, + const char *label, const uint8_t *buf, int n) { + if (level < current_level) return; + fprintf(stderr, "[%s] %s:%d %s: %s:", level_name(level), basename_of(file), line, func, label); + for (int i = 0; i < n; i++) { + fprintf(stderr, " %02x", buf[i]); + } + fputc('\n', stderr); +} diff --git a/log.h b/log.h new file mode 100644 index 0000000..0ce6f92 --- /dev/null +++ b/log.h @@ -0,0 +1,25 @@ +#ifndef LOG_H +#define LOG_H + +#include + +typedef enum { + LOG_LEVEL_DEBUG = 0, + LOG_LEVEL_INFO = 1, + LOG_LEVEL_WARN = 2, + LOG_LEVEL_ERROR = 3, + LOG_LEVEL_NONE = 4 +} LogLevel; + +void log_set_level(LogLevel level); +void log_print(LogLevel level, const char *file, int line, const char *func, const char *fmt, ...); +void log_bytes(LogLevel level, const char *file, int line, const char *func, const char *label, const uint8_t *buf, int n); + +#define log_debug(...) log_print(LOG_LEVEL_DEBUG, __FILE__, __LINE__, __func__, __VA_ARGS__) +#define log_info(...) log_print(LOG_LEVEL_INFO, __FILE__, __LINE__, __func__, __VA_ARGS__) +#define log_warn(...) log_print(LOG_LEVEL_WARN, __FILE__, __LINE__, __func__, __VA_ARGS__) +#define log_error(...) log_print(LOG_LEVEL_ERROR, __FILE__, __LINE__, __func__, __VA_ARGS__) +#define log_debug_bytes(lbl, b, n) log_bytes(LOG_LEVEL_DEBUG, __FILE__, __LINE__, __func__, (lbl), (const uint8_t *)(b), (n)) +#define log_warn_bytes(lbl, b, n) log_bytes(LOG_LEVEL_WARN, __FILE__, __LINE__, __func__, (lbl), (const uint8_t *)(b), (n)) + +#endif diff --git a/main.c b/main.c index f7615d7..e6590d4 100644 --- a/main.c +++ b/main.c @@ -7,6 +7,7 @@ #include "based.h" #include "bluetooth.h" +#include "log.h" #include "util.h" static const char *program_name; @@ -15,6 +16,8 @@ static void usage() { printf("Usage: %s [options]
\n" "\t-h, --help\n" "\t\tPrint the help message.\n" + "\t-D, --debug\n" + "\t\tEnable debug logging to stderr.\n" "\t-n , --name=\n" "\t\tChange the name of the device.\n" "\t-c , --noise-cancelling=\n" @@ -406,23 +409,24 @@ static int do_send_packet(int sock, const char *arg) { } } - uint8_t recieved[MAX_BT_PACK_LEN]; - int recieved_n = send_packet(sock, send, sizeof(send), recieved); - if (recieved_n < 0) { - return recieved_n; + uint8_t received[MAX_BT_PACK_LEN]; + int received_n = send_packet(sock, send, sizeof(send), received); + if (received_n < 0) { + return received_n; } - for (i = 0; i < recieved_n; ++i) { - printf("%02x ", recieved[i]); + for (i = 0; i < received_n; ++i) { + printf("%02x ", received[i]); } printf("\n"); return 0; } int main(int argc, char *argv[]) { - static const char *short_opt = "hn:l:v:o:c:dp:fsba"; + static const char *short_opt = "hn:l:v:o:c:dp:fsbaDG"; static const struct option long_opt[] = { { "help", no_argument, NULL, 'h' }, + { "debug", no_argument, NULL, 'D' }, { "name", required_argument, NULL, 'n' }, { "prompt-language", required_argument, NULL, 'l' }, { "voice-prompts", required_argument, NULL, 'v' }, @@ -443,7 +447,7 @@ int main(int argc, char *argv[]) { }; static const struct timeval send_timeout = { 5, 0 }; - static const struct timeval recieve_timeout = { 1, 0 }; + static const struct timeval receive_timeout = { 5, 0 }; int sock = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM); if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &send_timeout, sizeof(send_timeout)) < 0) { @@ -451,8 +455,8 @@ int main(int argc, char *argv[]) { return 1; } - if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &recieve_timeout, sizeof(recieve_timeout)) < 0) { - perror("Could not set socket recieve timeout"); + if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &receive_timeout, sizeof(receive_timeout)) < 0) { + perror("Could not set socket receive timeout"); return 1; } @@ -466,6 +470,9 @@ int main(int argc, char *argv[]) { case 'h': usage(); return 0; + case 'D': + log_set_level(LOG_LEVEL_DEBUG); + break; case '?': usage(); return 1; @@ -553,13 +560,18 @@ int main(int argc, char *argv[]) { case 1: status = do_send_packet(sock, optarg); break; + case 'D': + break; default: status = 1; } } - if (status < 0) { - perror("Error trying to change setting"); + if (status != 0) { + fprintf(stderr, "Command failed with status: %d\n", status); + if (status < 0) { + perror("Error trying to change setting"); + } } close(sock);