From 130b1c755dbfc95b31382d902ce6ac7f766e1308 Mon Sep 17 00:00:00 2001 From: john_mansell Date: Thu, 7 May 2026 13:42:57 -0700 Subject: [PATCH] Add NC 700 support, logging, and CMake build Note: This contribution was developed with AI assistance (Claude by Anthropic). Issue ----- The NC 700 was not recognised by the tool and the protocol handling assumed fixed-size responses, which broke on devices that return different payload lengths. Most set commands on the NC 700 do not send a confirmation packet, causing the tool to report failure even when the setting was applied. The original read() calls did not retry on short reads, making them fragile over RFCOMM. There was no way to inspect raw packet traffic when debugging a new device. Solution -------- Add explicit NC 700 support and harden the protocol layer to handle variable-length, length-prefixed responses. Treat EAGAIN on a confirmation read as success for devices that do not send one. Loop inside read() until exactly N bytes are received. Add a leveled logging system so packet-level traces can be enabled on demand without recompiling. Changes ------- - based.h: add DeviceId enum (QC35, QC35_II, SOUNDLINK_II, NC700); expand VER_STR_LEN from 6 to 32; fix send_packet parameter name typo. - based.c: add DEVICE_NC700 to has_noise_cancelling(); add read_exact() helper that retries short reads; refactor init_connection, get_device_id, get_firmware_version, and get_battery_level to read a 4-byte header and drain a length-prefixed payload; handle EAGAIN-as-success in all set commands; fix get_name mask to accept NC 700 encoding byte; simplify set_voice_prompts to not fetch full device status first; replace abs() difference comparisons with != for enum return values. - main.c: add -D / --debug flag wired to log_set_level(LOG_LEVEL_DEBUG); increase SO_RCVTIMEO from 1 s to 5 s; fix receive typo; improve error reporting when a command returns non-zero. - log.c / log.h: new leveled logging module (DEBUG/INFO/WARN/ERROR), off by default, writes to stderr with file/line/function prefix and a hex-dump helper for packet bytes. - CMakeLists.txt: new CMake build (C11, -Wall -Wextra -Wpedantic). - COMPATIBILITY.md: per-device feature matrix and per-device protocol notes covering QC35, SoundLink II, and NC 700. - README.txt: document -D flag, update disclaimer, expand todo list, add contributing / adding device support guide with HCI capture instructions. Co-Authored-By: Claude Sonnet 4.6 --- CMakeLists.txt | 24 ++++ COMPATIBILITY.md | 91 +++++++++++++ README.txt | 56 +++++++- based.c | 341 +++++++++++++++++++++++++++++++++-------------- based.h | 11 +- log.c | 47 +++++++ log.h | 25 ++++ main.c | 36 +++-- 8 files changed, 512 insertions(+), 119 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 COMPATIBILITY.md create mode 100644 log.c create mode 100644 log.h 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);