Files
2026-08-24 10:03:24 +08:00

339 lines
7.8 KiB
C

/*
* btd.inc
* vim: ft=c
*
* SPDX-FileCopyrightText: 2021-2025 BlueALSA developers
* SPDX-License-Identifier: MIT
*/
#if HAVE_CONFIG_H
# include <config.h>
#endif
#include <ctype.h>
#include <endian.h>
#include <errno.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#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;
}