/* * btd.inc * vim: ft=c * * SPDX-FileCopyrightText: 2021-2025 BlueALSA developers * SPDX-License-Identifier: MIT */ #if HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include "a2dp.h" #include "ba-transport.h" #include "hfp.h" #include "io.h" #include "utils.h" #include "shared/a2dp-codecs.h" #include "shared/defs.h" #include "shared/ffb.h" #include "shared/log.h" #define BT_DUMP_CURRENT_VERSION 2 /** * BT dump mode. */ enum bt_dump_mode { BT_DUMP_MODE_A2DP_SOURCE = 1, BT_DUMP_MODE_A2DP_SINK, BT_DUMP_MODE_ASHA_SOURCE, BT_DUMP_MODE_ASHA_SINK, BT_DUMP_MODE_SCO, }; /** * BT dump handle. */ struct bt_dump { unsigned int version; enum bt_dump_mode mode; uint32_t transport_codec_id; a2dp_t a2dp_configuration; size_t a2dp_configuration_size; FILE *file; }; /** * Read a single data record from a BT dump file. */ ssize_t bt_dump_read(struct bt_dump *btd, void *data, size_t size) { uint16_t len; if (fread(&len, sizeof(len), 1, btd->file) != 1) return -1; size = MIN(be16toh(len), size); if (fread(data, 1, size, btd->file) != size) return -1; return size; } /** * Write a single data record to a BT dump file. */ ssize_t bt_dump_write(struct bt_dump *btd, const void *data, size_t size) { uint16_t len = htobe16(size); if (fwrite(&len, sizeof(len), 1, btd->file) != 1) return -1; if (fwrite(data, 1, size, btd->file) != size) return -1; fflush(btd->file); return sizeof(len) + size; } /** * Close BT dump file. */ void bt_dump_close(struct bt_dump *btd) { if (btd == NULL) return; if (btd->file != NULL) fclose(btd->file); free(btd); } /** * Create BT dump file. */ struct bt_dump * bt_dump_create( const char * path, struct ba_transport * t) { struct bt_dump * btd; if ((btd = calloc(1, sizeof(*btd))) == NULL) return NULL; if ((btd->file = fopen(path, "wb")) == NULL) goto fail; enum bt_dump_mode mode = 0; const void * a2dp_configuration = NULL; size_t a2dp_configuration_size = 0; switch (t->profile) { case BA_TRANSPORT_PROFILE_NONE: #if ENABLE_MIDI case BA_TRANSPORT_PROFILE_MIDI: #endif break; case BA_TRANSPORT_PROFILE_A2DP_SOURCE: a2dp_configuration = &t->media.a2dp.configuration; a2dp_configuration_size = t->media.a2dp.sep->config.caps_size; mode = BT_DUMP_MODE_A2DP_SOURCE; break; case BA_TRANSPORT_PROFILE_A2DP_SINK: a2dp_configuration = &t->media.a2dp.configuration; a2dp_configuration_size = t->media.a2dp.sep->config.caps_size; mode = BT_DUMP_MODE_A2DP_SINK; break; #if ENABLE_ASHA case BA_TRANSPORT_PROFILE_ASHA_SOURCE: mode = BT_DUMP_MODE_ASHA_SOURCE; break; case BA_TRANSPORT_PROFILE_ASHA_SINK: mode = BT_DUMP_MODE_ASHA_SINK; break; #endif case BA_TRANSPORT_PROFILE_HFP_AG: case BA_TRANSPORT_PROFILE_HFP_HF: case BA_TRANSPORT_PROFILE_HSP_AG: case BA_TRANSPORT_PROFILE_HSP_HS: mode = BT_DUMP_MODE_SCO; break; } unsigned int mode_ = mode; fprintf(btd->file, "BA.dump-%1x.%1x.", BT_DUMP_CURRENT_VERSION, mode_); btd->transport_codec_id = ba_transport_get_codec(t); uint32_t id = htobe32(btd->transport_codec_id); if (bt_dump_write(btd, &id, sizeof(id)) == -1) goto fail; if (a2dp_configuration != NULL) { memcpy(&btd->a2dp_configuration, a2dp_configuration, MIN(a2dp_configuration_size, sizeof(btd->a2dp_configuration))); btd->a2dp_configuration_size = a2dp_configuration_size; if (bt_dump_write(btd, a2dp_configuration, a2dp_configuration_size) == -1) goto fail; } return btd; fail: bt_dump_close(btd); return NULL; } /** * Open BT dump file. */ struct bt_dump * bt_dump_open(const char * path) { struct bt_dump * btd; if ((btd = calloc(1, sizeof(*btd))) == NULL) return NULL; if ((btd->file = fopen(path, "rb")) == NULL) goto fail; unsigned int mode; if (fscanf(btd->file, "BA.dump-%1x.%1x.", &btd->version, &mode) != 2) { errno = EIO; goto fail; } if (btd->version > BT_DUMP_CURRENT_VERSION) { errno = EINVAL; goto fail; } uint32_t id; if (bt_dump_read(btd, &id, sizeof(id)) == -1) goto fail; btd->transport_codec_id = be32toh(id); switch (btd->mode = mode) { case BT_DUMP_MODE_A2DP_SOURCE: case BT_DUMP_MODE_A2DP_SINK: { ssize_t size = sizeof(btd->a2dp_configuration); if ((size = bt_dump_read(btd, &btd->a2dp_configuration, size)) == -1) goto fail; btd->a2dp_configuration_size = size; } break; #if ENABLE_ASHA case BT_DUMP_MODE_ASHA_SOURCE: case BT_DUMP_MODE_ASHA_SINK: #endif case BT_DUMP_MODE_SCO: break; default: errno = EINVAL; goto fail; } return btd; fail: bt_dump_close(btd); return NULL; } static const char * transport_to_fname(const struct ba_transport * t) { const uint32_t codec_id = ba_transport_get_codec(t); const char * profile = NULL; const char * codec = NULL; switch (t->profile) { case BA_TRANSPORT_PROFILE_NONE: return "none"; case BA_TRANSPORT_PROFILE_A2DP_SOURCE: profile = "a2dp-source"; codec = a2dp_codecs_codec_id_to_string(codec_id); break; case BA_TRANSPORT_PROFILE_A2DP_SINK: profile = "a2dp-sink"; codec = a2dp_codecs_codec_id_to_string(codec_id); break; #if ENABLE_ASHA case BA_TRANSPORT_PROFILE_ASHA_SOURCE: profile = "asha-source"; codec = asha_codec_id_to_string(codec_id); break; case BA_TRANSPORT_PROFILE_ASHA_SINK: profile = "asha-sink"; codec = asha_codec_id_to_string(codec_id); break; #endif case BA_TRANSPORT_PROFILE_HFP_AG: profile = "hfp-ag"; codec = hfp_codec_id_to_string(codec_id); break; case BA_TRANSPORT_PROFILE_HFP_HF: profile = "hfp-hf"; codec = hfp_codec_id_to_string(codec_id); break; case BA_TRANSPORT_PROFILE_HSP_AG: profile = "hsp-ag"; codec = hfp_codec_id_to_string(codec_id); break; case BA_TRANSPORT_PROFILE_HSP_HS: profile = "hsp-hs"; codec = hfp_codec_id_to_string(codec_id); break; #if ENABLE_MIDI case BA_TRANSPORT_PROFILE_MIDI: return "midi"; #endif } char fallback[16]; if (codec == NULL) { snprintf(fallback, sizeof(fallback), "%08x", codec_id); codec = fallback; } static char buffer[64]; snprintf(buffer, sizeof(buffer), "%s-%s", profile, codec); return buffer; } static const char *transport_pcm_to_fname(const struct ba_transport_pcm *t_pcm) { static char buffer[64]; snprintf(buffer, sizeof(buffer), "s%u-%u-%uc", BA_TRANSPORT_PCM_FORMAT_WIDTH(t_pcm->format), t_pcm->rate, t_pcm->channels); return buffer; } /** * Dump incoming BT data to a BT dump file. */ void *bt_dump_io_thread(struct ba_transport_pcm *t_pcm) { pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); pthread_cleanup_push(PTHREAD_CLEANUP(ba_transport_pcm_thread_cleanup), t_pcm); struct ba_transport *t = t_pcm->t; struct io_poll io = { .timeout = -1 }; ffb_t bt = { 0 }; struct bt_dump *btd = NULL; char fname[256]; snprintf(fname, sizeof(fname), "/tmp/bluealsa-%s-%s.btd", transport_to_fname(t), transport_pcm_to_fname(t_pcm)); debug("Creating BT dump file: %s", fname); if ((btd = bt_dump_create(fname, t)) == NULL) { error("Couldn't create BT dump file: %s", strerror(errno)); goto fail_open; } pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &bt); pthread_cleanup_push(PTHREAD_CLEANUP(bt_dump_close), btd); if (ffb_init_uint8_t(&bt, t->mtu_read) == -1) { error("Couldn't create data buffer: %s", strerror(errno)); goto fail_ffb; } debug_transport_pcm_thread_loop(t_pcm, "START"); for (ba_transport_pcm_state_set_running(t_pcm);;) { ssize_t len; ffb_rewind(&bt); if ((len = io_poll_and_read_bt(&io, t_pcm, &bt)) <= 0) { if (len == -1) error("BT poll and read error: %s", strerror(errno)); goto fail; } debug("BT read: %zd", len); bt_dump_write(btd, bt.data, len); } fail: debug_transport_pcm_thread_loop(t_pcm, "EXIT"); fail_ffb: pthread_cleanup_pop(1); pthread_cleanup_pop(1); fail_open: pthread_cleanup_pop(1); return NULL; }