blob: a954869b0a6636698c5f9b80e0f02cd88a5733fa [file]
// SPDX-License-Identifier: LGPL-2.1-or-later
/*
*
* BlueZ - Bluetooth protocol stack for Linux
*
* Copyright (C) 2022 Intel Corporation. All rights reserved.
* Copyright (C) 2025 Pauli Virtanen. All rights reserved.
*
*/
#define _GNU_SOURCE
#include <inttypes.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <errno.h>
#include "bluetooth/bluetooth.h"
#include "bluetooth/uuid.h"
#include "bluetooth/hci.h"
#include "src/shared/queue.h"
#include "src/shared/util.h"
#include "src/shared/timeout.h"
#include "src/shared/att.h"
#include "src/shared/gatt-db.h"
#include "src/shared/gatt-server.h"
#include "src/shared/gatt-client.h"
#include "src/shared/mcp.h"
#include "src/shared/mcs.h"
#define BT_MCS_ERROR_VALUE_CHANGED_DURING_READ_LONG 0x80
#define DBG_MCP(mcp, fmt, ...) \
mcp_debug(mcp, "%s:%s() mcp %p | " fmt, __FILE__, __func__, mcp, \
##__VA_ARGS__)
#define DBG_SVC(service, fmt, ...) \
mcp_debug(service->mcp, "%s:%s() svc %p | " fmt, __FILE__, __func__, \
service, ##__VA_ARGS__)
#define DBG_MCS(mcs, fmt, ...) \
mcs_debug(mcs, "%s:%s() mcs %p | " fmt, __FILE__, __func__, mcs, \
##__VA_ARGS__)
#define MAX_ATTR 32
#define MAX_PENDING 256
struct bt_mcs_db {
bool gmcs;
int ccid_value;
uint32_t media_cp_op_supported_value;
uint16_t playing_order_supported_value;
struct gatt_db_attribute *service;
struct gatt_db_attribute *media_player_name;
struct gatt_db_attribute *media_player_name_ccc;
struct gatt_db_attribute *track_changed;
struct gatt_db_attribute *track_changed_ccc;
struct gatt_db_attribute *track_title;
struct gatt_db_attribute *track_title_ccc;
struct gatt_db_attribute *track_duration;
struct gatt_db_attribute *track_duration_ccc;
struct gatt_db_attribute *track_position;
struct gatt_db_attribute *track_position_ccc;
struct gatt_db_attribute *playback_speed;
struct gatt_db_attribute *playback_speed_ccc;
struct gatt_db_attribute *seeking_speed;
struct gatt_db_attribute *seeking_speed_ccc;
struct gatt_db_attribute *playing_order;
struct gatt_db_attribute *playing_order_ccc;
struct gatt_db_attribute *playing_order_supported;
struct gatt_db_attribute *media_state;
struct gatt_db_attribute *media_state_ccc;
struct gatt_db_attribute *media_cp;
struct gatt_db_attribute *media_cp_ccc;
struct gatt_db_attribute *media_cp_op_supported;
struct gatt_db_attribute *media_cp_op_supported_ccc;
struct gatt_db_attribute *ccid;
};
struct bt_mcs_session {
struct bt_mcs *mcs;
struct bt_att *att;
unsigned int disconn_id;
/* Per-client state */
struct queue *changed;
};
struct bt_mcs {
struct gatt_db *db;
struct bt_mcs_db ldb;
struct queue *sessions;
uint8_t media_state;
const struct bt_mcs_callback *cb;
void *user_data;
};
struct bt_mcp_listener {
const struct bt_mcp_listener_callback *cb;
void *user_data;
};
struct bt_mcp_service {
struct bt_mcp *mcp;
struct bt_mcs_db rdb;
bool ready;
unsigned int notify_id[MAX_ATTR];
unsigned int notify_id_count;
unsigned int pending_id;
struct queue *pending;
struct queue *listeners;
};
struct bt_mcp_pending {
struct bt_mcp_service *service;
unsigned int id;
uint8_t op;
struct {
unsigned int client_id;
struct gatt_db_attribute *attrib;
uint8_t result;
} write;
};
struct bt_mcp {
bool gmcs;
struct bt_gatt_client *client;
unsigned int idle_id;
unsigned int db_id;
bool ready;
struct queue *services;
const struct bt_mcp_callback *cb;
void *user_data;
};
#define MCS_COMMAND(name_, op_, arg_, end_state_) \
{ \
.name = name_, \
.op = BT_MCS_CMD_ ## op_, \
.support = BT_MCS_CMD_ ## op_ ## _SUPPORTED, \
.int32_arg = arg_, \
.end_state = end_state_, \
}
#define ANY_STATE -1
static const struct mcs_command {
const char *name;
uint8_t op;
uint32_t support;
bool int32_arg;
int end_state;
} mcs_command[] = {
MCS_COMMAND("Play", PLAY, false, BT_MCS_STATE_PLAYING),
MCS_COMMAND("Pause", PAUSE, false, BT_MCS_STATE_PAUSED),
MCS_COMMAND("Fast Rewind", FAST_REWIND, false, BT_MCS_STATE_SEEKING),
MCS_COMMAND("Fast Forward", FAST_FORWARD, false, BT_MCS_STATE_SEEKING),
MCS_COMMAND("Stop", STOP, false, BT_MCS_STATE_PAUSED),
MCS_COMMAND("Move Relative", MOVE_RELATIVE, true, ANY_STATE),
MCS_COMMAND("Prev Segment", PREV_SEGMENT, false, ANY_STATE),
MCS_COMMAND("Next Segment", NEXT_SEGMENT, false, ANY_STATE),
MCS_COMMAND("First Segment", FIRST_SEGMENT, false, ANY_STATE),
MCS_COMMAND("Last Segment", LAST_SEGMENT, false, ANY_STATE),
MCS_COMMAND("Goto Segment", GOTO_SEGMENT, true, ANY_STATE),
MCS_COMMAND("Prev Track", PREV_TRACK, false, ANY_STATE),
MCS_COMMAND("Next Track", NEXT_TRACK, false, ANY_STATE),
MCS_COMMAND("First Track", FIRST_TRACK, false, ANY_STATE),
MCS_COMMAND("Last Track", LAST_TRACK, false, ANY_STATE),
MCS_COMMAND("Goto Track", GOTO_TRACK, true, ANY_STATE),
MCS_COMMAND("Prev Group", PREV_GROUP, false, ANY_STATE),
MCS_COMMAND("Next Group", NEXT_GROUP, false, ANY_STATE),
MCS_COMMAND("First Group", FIRST_GROUP, false, ANY_STATE),
MCS_COMMAND("Last Group", LAST_GROUP, false, ANY_STATE),
MCS_COMMAND("Goto Group", GOTO_GROUP, true, ANY_STATE),
};
#define MCS_PLAYING_ORDER(name) \
{ BT_MCS_ORDER_ ## name, BT_MCS_ORDER_SUPPORTED_ ## name }
static const struct {
uint8_t order;
uint16_t support;
} mcs_playing_orders[] = {
MCS_PLAYING_ORDER(SINGLE_ONCE),
MCS_PLAYING_ORDER(SINGLE_REPEAT),
MCS_PLAYING_ORDER(IN_ORDER_ONCE),
MCS_PLAYING_ORDER(IN_ORDER_REPEAT),
MCS_PLAYING_ORDER(OLDEST_ONCE),
MCS_PLAYING_ORDER(OLDEST_REPEAT),
MCS_PLAYING_ORDER(NEWEST_ONCE),
MCS_PLAYING_ORDER(NEWEST_REPEAT),
MCS_PLAYING_ORDER(SHUFFLE_ONCE),
MCS_PLAYING_ORDER(SHUFFLE_REPEAT)
};
typedef bool (*mcs_command_func_t)(void *data);
typedef bool (*mcs_command_func_int32_t)(void *data, int32_t offset);
typedef void (*mcs_get_func_t)(struct bt_mcs *mcs, struct iovec *buf,
size_t size);
typedef bool (*mcs_set_func_t)(struct bt_mcs *mcs, void *data);
static struct queue *servers;
static uint8_t servers_ccid;
/*
* MCS Server
*/
static void mcs_debug_func(const char *str, void *user_data)
{
struct bt_mcs *mcs = user_data;
mcs->cb->debug(mcs->user_data, str);
}
static void mcs_debug(struct bt_mcs *mcs, const char *format, ...)
{
va_list ap;
if (!mcs || !format || !mcs->cb->debug)
return;
va_start(ap, format);
util_debug_va(mcs_debug_func, mcs, format, ap);
va_end(ap);
}
static const struct mcs_command *mcs_get_command(uint8_t op)
{
size_t i;
for (i = 0; i < ARRAY_SIZE(mcs_command); ++i)
if (mcs_command[i].op == op)
return &mcs_command[i];
return NULL;
}
static mcs_command_func_t mcs_get_handler(struct bt_mcs *mcs, uint8_t op)
{
switch (op) {
case BT_MCS_CMD_PLAY: return mcs->cb->play;
case BT_MCS_CMD_PAUSE: return mcs->cb->pause;
case BT_MCS_CMD_FAST_REWIND: return mcs->cb->fast_rewind;
case BT_MCS_CMD_FAST_FORWARD: return mcs->cb->fast_forward;
case BT_MCS_CMD_STOP: return mcs->cb->stop;
case BT_MCS_CMD_PREV_SEGMENT: return mcs->cb->previous_segment;
case BT_MCS_CMD_NEXT_SEGMENT: return mcs->cb->next_segment;
case BT_MCS_CMD_FIRST_SEGMENT: return mcs->cb->first_segment;
case BT_MCS_CMD_LAST_SEGMENT: return mcs->cb->last_segment;
case BT_MCS_CMD_PREV_TRACK: return mcs->cb->previous_track;
case BT_MCS_CMD_NEXT_TRACK: return mcs->cb->next_track;
case BT_MCS_CMD_FIRST_TRACK: return mcs->cb->first_track;
case BT_MCS_CMD_LAST_TRACK: return mcs->cb->last_track;
case BT_MCS_CMD_PREV_GROUP: return mcs->cb->previous_group;
case BT_MCS_CMD_NEXT_GROUP: return mcs->cb->next_group;
case BT_MCS_CMD_FIRST_GROUP: return mcs->cb->first_group;
case BT_MCS_CMD_LAST_GROUP: return mcs->cb->last_group;
}
return NULL;
}
static mcs_command_func_int32_t mcs_get_handler_int32(struct bt_mcs *mcs,
uint8_t op)
{
switch (op) {
case BT_MCS_CMD_MOVE_RELATIVE: return mcs->cb->move_relative;
case BT_MCS_CMD_GOTO_SEGMENT: return mcs->cb->goto_segment;
case BT_MCS_CMD_GOTO_TRACK: return mcs->cb->goto_track;
case BT_MCS_CMD_GOTO_GROUP: return mcs->cb->goto_group;
}
return NULL;
}
static uint32_t mcs_get_supported(struct bt_mcs *mcs)
{
unsigned int i;
uint32_t value = 0;
for (i = 0; i < ARRAY_SIZE(mcs_command); ++i)
value |= mcs_command[i].support;
if (mcs->cb->media_cp_op_supported)
value = mcs->cb->media_cp_op_supported(mcs->user_data);
for (i = 0; i < ARRAY_SIZE(mcs_command); ++i) {
void *handler = mcs_get_handler(mcs, mcs_command[i].op);
if (!handler)
handler = mcs_get_handler_int32(mcs, mcs_command[i].op);
if (!handler)
value &= ~mcs_command[i].support;
}
mcs->ldb.media_cp_op_supported_value = value;
return value;
}
static void write_media_cp(struct gatt_db_attribute *attrib,
unsigned int id, uint16_t offset,
const uint8_t *data, size_t len,
uint8_t opcode, struct bt_att *att,
void *user_data)
{
struct bt_mcs *mcs = user_data;
struct iovec iov = { .iov_base = (void *)data, .iov_len = len };
const struct mcs_command *cmd = NULL;
struct bt_mcs_cp_rsp rsp = {
.op = 0,
.result = BT_MCS_RESULT_COMMAND_CANNOT_COMPLETE
};
int ret = 0;
int32_t arg = 0;
uint8_t op;
bool ok = false;
if (offset) {
ret = BT_ATT_ERROR_INVALID_OFFSET;
goto respond;
}
if (!util_iov_pull_u8(&iov, &op)) {
ret = BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN;
goto respond;
}
rsp.op = op;
cmd = mcs_get_command(op);
if (!cmd || !(cmd->support & mcs_get_supported(mcs))) {
rsp.result = BT_MCS_RESULT_OP_NOT_SUPPORTED;
goto respond;
}
if (cmd->int32_arg && !util_iov_pull_le32(&iov, (uint32_t *)&arg)) {
ret = BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN;
rsp.op = 0;
goto respond;
}
DBG_MCS(mcs, "Command %s", cmd->name);
/* We may attempt to perform the operation also if inactive (MCS v1.0.1
* p. 26), leave decision to upper layer.
*/
ok = cmd->int32_arg ?
mcs_get_handler_int32(mcs, op)(mcs->user_data, arg) :
mcs_get_handler(mcs, op)(mcs->user_data);
if (ok)
rsp.result = BT_MCS_RESULT_SUCCESS;
else if (mcs->media_state == BT_MCS_STATE_INACTIVE)
rsp.result = BT_MCS_RESULT_MEDIA_PLAYER_INACTIVE;
else
rsp.result = BT_MCS_RESULT_COMMAND_CANNOT_COMPLETE;
respond:
DBG_MCS(mcs, "%s ret %u result %u", cmd ? cmd->name : "-",
ret, rsp.result);
gatt_db_attribute_write_result(attrib, id, ret);
if (!rsp.op)
return;
/* Make state transition immediately if command was successful and has
* specified end state. Upper layer shall emit spontaneous transitions
* to correct as needed.
*/
if (ok) {
bt_mcs_set_media_state(mcs, cmd->end_state);
switch (op) {
case BT_MCS_CMD_STOP:
case BT_MCS_CMD_PREV_TRACK:
case BT_MCS_CMD_NEXT_TRACK:
case BT_MCS_CMD_FIRST_TRACK:
case BT_MCS_CMD_LAST_TRACK:
case BT_MCS_CMD_GOTO_TRACK:
case BT_MCS_CMD_PREV_GROUP:
case BT_MCS_CMD_NEXT_GROUP:
case BT_MCS_CMD_FIRST_GROUP:
case BT_MCS_CMD_LAST_GROUP:
case BT_MCS_CMD_GOTO_GROUP:
if (mcs->cb->set_track_position)
mcs->cb->set_track_position(mcs->user_data, 0);
bt_mcs_changed(mcs, MCS_TRACK_POSITION_CHRC_UUID);
break;
}
}
gatt_db_attribute_notify(attrib, (uint8_t *)&rsp, sizeof(rsp), att);
}
void bt_mcs_set_media_state(struct bt_mcs *mcs, uint8_t state)
{
switch (state) {
case BT_MCS_STATE_INACTIVE:
case BT_MCS_STATE_PLAYING:
case BT_MCS_STATE_PAUSED:
case BT_MCS_STATE_SEEKING:
break;
default:
return;
}
if (state == mcs->media_state)
return;
mcs->media_state = state;
bt_mcs_changed(mcs, MCS_MEDIA_STATE_CHRC_UUID);
}
uint8_t bt_mcs_get_media_state(struct bt_mcs *mcs)
{
return mcs->media_state;
}
static void get_media_player_name(struct bt_mcs *mcs, struct iovec *buf,
size_t size)
{
if (mcs->cb->media_player_name)
mcs->cb->media_player_name(mcs->user_data, buf, size);
}
static void get_track_changed(struct bt_mcs *mcs, struct iovec *buf,
size_t size)
{
}
static void get_track_title(struct bt_mcs *mcs, struct iovec *buf,
size_t size)
{
if (mcs->cb->track_title)
mcs->cb->track_title(mcs->user_data, buf, size);
}
static void get_track_duration(struct bt_mcs *mcs, struct iovec *buf,
size_t size)
{
int32_t value = BT_MCS_DURATION_UNAVAILABLE;
if (mcs->cb->track_duration)
value = mcs->cb->track_duration(mcs->user_data);
util_iov_push_le32(buf, (uint32_t)value);
}
static void get_track_position(struct bt_mcs *mcs, struct iovec *buf,
size_t size)
{
int32_t value = BT_MCS_POSITION_UNAVAILABLE;
if (mcs->cb->track_position)
value = mcs->cb->track_position(mcs->user_data);
util_iov_push_le32(buf, (uint32_t)value);
}
static void get_playback_speed(struct bt_mcs *mcs, struct iovec *buf,
size_t size)
{
int8_t value = 0x00;
if (mcs->cb->playback_speed)
value = mcs->cb->playback_speed(mcs->user_data);
util_iov_push_u8(buf, (uint8_t)value);
}
static void get_seeking_speed(struct bt_mcs *mcs, struct iovec *buf,
size_t size)
{
int8_t value = 0x00;
if (mcs->cb->seeking_speed)
value = mcs->cb->seeking_speed(mcs->user_data);
util_iov_push_u8(buf, (uint8_t)value);
}
static void get_playing_order(struct bt_mcs *mcs, struct iovec *buf,
size_t size)
{
uint8_t value = BT_MCS_ORDER_IN_ORDER_REPEAT;
if (mcs->cb->playing_order)
value = mcs->cb->playing_order(mcs->user_data);
util_iov_push_u8(buf, value);
}
static void get_playing_order_supported(struct bt_mcs *mcs, struct iovec *buf,
size_t size)
{
uint16_t value = BT_MCS_ORDER_SUPPORTED_IN_ORDER_REPEAT;
if (mcs->cb->playing_order_supported)
value = mcs->cb->playing_order_supported(mcs->user_data);
util_iov_push_le16(buf, value);
}
static void get_media_state(struct bt_mcs *mcs, struct iovec *buf,
size_t size)
{
util_iov_push_u8(buf, mcs->media_state);
}
static void get_media_cp_op_supported(struct bt_mcs *mcs, struct iovec *buf,
size_t size)
{
util_iov_push_le32(buf, mcs_get_supported(mcs));
}
static void get_ccid(struct bt_mcs *mcs, struct iovec *buf, size_t size)
{
util_iov_push_u8(buf, mcs->ldb.ccid_value);
}
static bool set_track_position(struct bt_mcs *mcs, void *data)
{
int32_t value = (int32_t)get_le32(data);
DBG_MCS(mcs, "Set Track Position %d", value);
if (mcs->cb->set_track_position)
return mcs->cb->set_track_position(mcs->user_data, value);
return false;
}
static bool set_playback_speed(struct bt_mcs *mcs, void *data)
{
int8_t value = (int8_t)get_u8(data);
DBG_MCS(mcs, "Set Playback Speed %d", value);
if (mcs->cb->set_playback_speed)
return mcs->cb->set_playback_speed(mcs->user_data, value);
return false;
}
static bool set_playing_order(struct bt_mcs *mcs, void *data)
{
uint8_t value = get_u8(data);
DBG_MCS(mcs, "Set Playing Order %u", value);
if (mcs->cb->set_playing_order)
return mcs->cb->set_playing_order(mcs->user_data, value);
return false;
}
static bool match_session_att(const void *data, const void *match_data)
{
const struct bt_mcs_session *session = data;
return session->att == match_data;
}
static void session_destroy(void *data)
{
struct bt_mcs_session *session = data;
struct bt_mcs *mcs = session->mcs;
if (mcs)
queue_remove(mcs->sessions, session);
queue_destroy(session->changed, NULL);
free(session);
}
static void session_remove(void *user_data)
{
struct bt_mcs_session *session = user_data;
session->mcs = NULL;
bt_att_unregister_disconnect(session->att, session->disconn_id);
}
static struct bt_mcs_session *get_session(struct bt_mcs *mcs,
struct bt_att *att)
{
struct bt_mcs_session *session;
session = queue_find(mcs->sessions, match_session_att, att);
if (session)
return session;
session = new0(struct bt_mcs_session, 1);
session->disconn_id = bt_att_register_disconnect(att, NULL, session,
session_destroy);
if (!session->disconn_id) {
free(session);
return NULL;
}
session->mcs = mcs;
session->att = att;
session->changed = queue_new();
queue_push_tail(mcs->sessions, session);
return session;
}
static void session_changed(void *data, void *user_data)
{
struct bt_mcs_session *session = data;
struct gatt_db_attribute *attrib = user_data;
if (!queue_find(session->changed, NULL, attrib))
queue_push_tail(session->changed, attrib);
}
static void read_result(struct bt_mcs *mcs, struct gatt_db_attribute *attrib,
unsigned int id, uint16_t offset, struct bt_att *att,
mcs_get_func_t get)
{
uint8_t buf[BT_ATT_MAX_VALUE_LEN];
struct iovec iov = { .iov_base = buf, .iov_len = 0 };
struct bt_mcs_session *session = get_session(mcs, att);
if (!session) {
gatt_db_attribute_read_result(attrib, id,
BT_ATT_ERROR_UNLIKELY, NULL, 0);
return;
}
if (!offset) {
queue_remove(session->changed, attrib);
} else if (queue_find(session->changed, NULL, attrib)) {
gatt_db_attribute_read_result(attrib, id,
BT_MCS_ERROR_VALUE_CHANGED_DURING_READ_LONG, NULL, 0);
return;
}
get(mcs, &iov, sizeof(buf));
if (offset > iov.iov_len) {
gatt_db_attribute_read_result(attrib, id,
BT_ATT_ERROR_INVALID_OFFSET, NULL, 0);
return;
}
gatt_db_attribute_read_result(attrib, id, 0, buf + offset,
iov.iov_len - offset);
}
#define READ_FUNC(name) \
static void read_ ## name(struct gatt_db_attribute *attrib, \
unsigned int id, uint16_t offset, \
uint8_t opcode, struct bt_att *att, \
void *user_data) \
{ \
DBG_MCS(user_data, ""); \
read_result(user_data, attrib, id, offset, att, get_ ##name); \
}
READ_FUNC(media_player_name)
READ_FUNC(track_title)
READ_FUNC(track_duration)
READ_FUNC(track_position)
READ_FUNC(playback_speed)
READ_FUNC(seeking_speed)
READ_FUNC(playing_order)
READ_FUNC(playing_order_supported)
READ_FUNC(media_state)
READ_FUNC(media_cp_op_supported)
READ_FUNC(ccid)
static void write_result(struct bt_mcs *mcs,
struct gatt_db_attribute *attrib,
unsigned int id, uint16_t offset,
const uint8_t *data, size_t len,
mcs_get_func_t get, mcs_set_func_t set)
{
uint8_t buf[4];
struct iovec iov = { .iov_base = buf, .iov_len = 0 };
bt_uuid_t uuid;
uint8_t ret;
get(mcs, &iov, sizeof(buf));
if (len > iov.iov_len) {
gatt_db_attribute_write_result(attrib, id,
BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN);
return;
}
if (offset + len > iov.iov_len) {
gatt_db_attribute_write_result(attrib, id,
BT_ATT_ERROR_INVALID_OFFSET);
return;
}
memcpy(iov.iov_base + offset, data, len);
if (set(mcs, iov.iov_base))
ret = 0;
else
ret = BT_ATT_ERROR_VALUE_NOT_ALLOWED;
gatt_db_attribute_write_result(attrib, id, ret);
if (!gatt_db_attribute_get_char_data(attrib, NULL, NULL, NULL, NULL,
&uuid))
return;
if (!ret)
bt_mcs_changed(mcs, uuid.value.u16);
}
#define WRITE_FUNC(name) \
static void write_ ## name(struct gatt_db_attribute *attrib, \
unsigned int id, uint16_t offset, \
const uint8_t *data, size_t len, \
uint8_t opcode, struct bt_att *att, \
void *user_data) \
{ write_result(user_data, attrib, id, offset, data, len, \
get_ ## name, set_ ## name); }
WRITE_FUNC(track_position)
WRITE_FUNC(playback_speed)
WRITE_FUNC(playing_order)
void bt_mcs_changed(struct bt_mcs *mcs, uint16_t chrc_uuid)
{
struct {
struct gatt_db_attribute *attr;
mcs_get_func_t get;
} attrs[] = {
{ mcs->ldb.media_player_name, get_media_player_name },
{ mcs->ldb.track_changed, get_track_changed },
{ mcs->ldb.track_title, get_track_title },
{ mcs->ldb.track_duration, get_track_duration },
{ mcs->ldb.track_position, get_track_position },
{ mcs->ldb.playback_speed, get_playback_speed },
{ mcs->ldb.seeking_speed, get_seeking_speed },
{ mcs->ldb.playing_order, get_playing_order },
{ mcs->ldb.media_state, get_media_state },
{ mcs->ldb.media_cp_op_supported, get_media_cp_op_supported },
};
uint8_t buf[BT_ATT_MAX_VALUE_LEN];
struct iovec iov = { .iov_base = buf, .iov_len = 0 };
unsigned int i;
bt_uuid_t uuid, uuid_attr;
uint8_t props;
bt_uuid16_create(&uuid, chrc_uuid);
for (i = 0; i < ARRAY_SIZE(attrs); ++i) {
if (!gatt_db_attribute_get_char_data(attrs[i].attr, NULL,
NULL, &props, NULL, &uuid_attr))
continue;
if (bt_uuid_cmp(&uuid_attr, &uuid))
continue;
queue_foreach(mcs->sessions, session_changed, attrs[i].attr);
DBG_MCS(mcs, "Notify 0x%04x", chrc_uuid);
attrs[i].get(mcs, &iov, sizeof(buf));
/* No client-specific state: notify everyone */
gatt_db_attribute_notify(attrs[i].attr, iov.iov_base,
iov.iov_len, NULL);
break;
}
}
static bool mcs_init_db(struct bt_mcs *mcs, bool is_gmcs)
{
struct gatt_db *db = mcs->db;
struct bt_mcs_db *ldb = &mcs->ldb;
bt_uuid_t uuid;
bt_uuid16_create(&uuid, is_gmcs ? GMCS_UUID : MCS_UUID);
ldb->service = gatt_db_add_service(db, &uuid, true, 38);
/* Add also optional CCC */
bt_uuid16_create(&uuid, MCS_MEDIA_PLAYER_NAME_CHRC_UUID);
ldb->media_player_name = gatt_db_service_add_characteristic(
ldb->service, &uuid,
BT_ATT_PERM_READ,
BT_GATT_CHRC_PROP_READ | BT_GATT_CHRC_PROP_NOTIFY,
read_media_player_name, NULL, mcs);
ldb->media_player_name_ccc = gatt_db_service_add_ccc(
ldb->service, BT_ATT_PERM_READ | BT_ATT_PERM_WRITE);
bt_uuid16_create(&uuid, MCS_TRACK_CHANGED_CHRC_UUID);
ldb->track_changed = gatt_db_service_add_characteristic(
ldb->service, &uuid,
BT_ATT_PERM_NONE, BT_GATT_CHRC_PROP_NOTIFY,
NULL, NULL, mcs);
gatt_db_attribute_set_fixed_length(ldb->track_changed, 0);
ldb->track_changed_ccc = gatt_db_service_add_ccc(
ldb->service, BT_ATT_PERM_READ | BT_ATT_PERM_WRITE);
bt_uuid16_create(&uuid, MCS_TRACK_TITLE_CHRC_UUID);
ldb->track_title = gatt_db_service_add_characteristic(
ldb->service, &uuid,
BT_ATT_PERM_READ,
BT_GATT_CHRC_PROP_READ | BT_GATT_CHRC_PROP_NOTIFY,
read_track_title, NULL, mcs);
ldb->track_title_ccc = gatt_db_service_add_ccc(
ldb->service, BT_ATT_PERM_READ | BT_ATT_PERM_WRITE);
bt_uuid16_create(&uuid, MCS_TRACK_DURATION_CHRC_UUID);
ldb->track_duration = gatt_db_service_add_characteristic(
ldb->service, &uuid,
BT_ATT_PERM_READ,
BT_GATT_CHRC_PROP_READ | BT_GATT_CHRC_PROP_NOTIFY,
read_track_duration, NULL, mcs);
gatt_db_attribute_set_fixed_length(ldb->track_duration,
sizeof(int32_t));
ldb->track_duration_ccc = gatt_db_service_add_ccc(
ldb->service, BT_ATT_PERM_READ | BT_ATT_PERM_WRITE);
bt_uuid16_create(&uuid, MCS_TRACK_POSITION_CHRC_UUID);
ldb->track_position = gatt_db_service_add_characteristic(
ldb->service, &uuid,
BT_ATT_PERM_READ | BT_ATT_PERM_WRITE,
BT_GATT_CHRC_PROP_READ | BT_GATT_CHRC_PROP_NOTIFY |
BT_GATT_CHRC_PROP_WRITE | BT_GATT_CHRC_PROP_WRITE_WITHOUT_RESP,
read_track_position, write_track_position, mcs);
gatt_db_attribute_set_fixed_length(ldb->track_position,
sizeof(int32_t));
ldb->track_position_ccc = gatt_db_service_add_ccc(
ldb->service, BT_ATT_PERM_READ | BT_ATT_PERM_WRITE);
bt_uuid16_create(&uuid, MCS_PLAYBACK_SPEED_CHRC_UUID);
ldb->playback_speed = gatt_db_service_add_characteristic(
ldb->service, &uuid,
BT_ATT_PERM_READ | BT_ATT_PERM_WRITE,
BT_GATT_CHRC_PROP_READ | BT_GATT_CHRC_PROP_NOTIFY |
BT_GATT_CHRC_PROP_WRITE | BT_GATT_CHRC_PROP_WRITE_WITHOUT_RESP,
read_playback_speed, write_playback_speed, mcs);
gatt_db_attribute_set_fixed_length(ldb->playback_speed, sizeof(int8_t));
ldb->playback_speed_ccc = gatt_db_service_add_ccc(
ldb->service, BT_ATT_PERM_READ | BT_ATT_PERM_WRITE);
bt_uuid16_create(&uuid, MCS_SEEKING_SPEED_CHRC_UUID);
ldb->seeking_speed = gatt_db_service_add_characteristic(
ldb->service, &uuid,
BT_ATT_PERM_READ,
BT_GATT_CHRC_PROP_READ | BT_GATT_CHRC_PROP_NOTIFY,
read_seeking_speed, NULL, mcs);
gatt_db_attribute_set_fixed_length(ldb->seeking_speed, sizeof(int8_t));
ldb->seeking_speed_ccc = gatt_db_service_add_ccc(
ldb->service, BT_ATT_PERM_READ | BT_ATT_PERM_WRITE);
bt_uuid16_create(&uuid, MCS_PLAYING_ORDER_CHRC_UUID);
ldb->playing_order = gatt_db_service_add_characteristic(
ldb->service, &uuid,
BT_ATT_PERM_READ | BT_ATT_PERM_WRITE,
BT_GATT_CHRC_PROP_READ | BT_GATT_CHRC_PROP_NOTIFY |
BT_GATT_CHRC_PROP_WRITE | BT_GATT_CHRC_PROP_WRITE_WITHOUT_RESP,
read_playing_order, write_playing_order, mcs);
gatt_db_attribute_set_fixed_length(ldb->playing_order, sizeof(uint8_t));
ldb->playing_order_ccc = gatt_db_service_add_ccc(
ldb->service, BT_ATT_PERM_READ | BT_ATT_PERM_WRITE);
bt_uuid16_create(&uuid, MCS_PLAYING_ORDER_SUPPORTED_CHRC_UUID);
ldb->playing_order_supported = gatt_db_service_add_characteristic(
ldb->service, &uuid,
BT_ATT_PERM_READ, BT_GATT_CHRC_PROP_READ,
read_playing_order_supported, NULL, mcs);
gatt_db_attribute_set_fixed_length(ldb->playing_order_supported,
sizeof(uint16_t));
bt_uuid16_create(&uuid, MCS_MEDIA_STATE_CHRC_UUID);
ldb->media_state = gatt_db_service_add_characteristic(
ldb->service, &uuid,
BT_ATT_PERM_READ,
BT_GATT_CHRC_PROP_READ | BT_GATT_CHRC_PROP_NOTIFY,
read_media_state, NULL, mcs);
gatt_db_attribute_set_fixed_length(ldb->media_state, sizeof(uint8_t));
ldb->media_state_ccc = gatt_db_service_add_ccc(
ldb->service, BT_ATT_PERM_READ | BT_ATT_PERM_WRITE);
bt_uuid16_create(&uuid, MCS_MEDIA_CP_CHRC_UUID);
ldb->media_cp = gatt_db_service_add_characteristic(
ldb->service, &uuid,
BT_ATT_PERM_WRITE,
BT_GATT_CHRC_PROP_WRITE | BT_GATT_CHRC_PROP_NOTIFY |
BT_GATT_CHRC_PROP_WRITE_WITHOUT_RESP,
NULL, write_media_cp, mcs);
ldb->media_cp_ccc = gatt_db_service_add_ccc(
ldb->service, BT_ATT_PERM_READ | BT_ATT_PERM_WRITE);
bt_uuid16_create(&uuid, MCS_MEDIA_CP_OP_SUPPORTED_CHRC_UUID);
ldb->media_cp_op_supported = gatt_db_service_add_characteristic(
ldb->service, &uuid,
BT_ATT_PERM_READ,
BT_GATT_CHRC_PROP_READ | BT_GATT_CHRC_PROP_NOTIFY,
read_media_cp_op_supported, NULL, mcs);
gatt_db_attribute_set_fixed_length(ldb->media_cp_op_supported,
sizeof(uint32_t));
ldb->media_cp_op_supported_ccc = gatt_db_service_add_ccc(
ldb->service, BT_ATT_PERM_READ | BT_ATT_PERM_WRITE);
bt_uuid16_create(&uuid, MCS_CCID_CHRC_UUID);
ldb->ccid = gatt_db_service_add_characteristic(
ldb->service, &uuid,
BT_ATT_PERM_READ, BT_GATT_CHRC_PROP_READ,
read_ccid, NULL, mcs);
gatt_db_attribute_set_fixed_length(ldb->ccid, sizeof(uint8_t));
return true;
}
uint8_t bt_mcs_get_ccid(struct bt_mcs *mcs)
{
return mcs->ldb.ccid_value;
}
struct match_mcs_data {
struct gatt_db *db;
bool gmcs;
bool any;
int ccid;
};
static bool match_mcs(const void *data, const void *user_data)
{
const struct bt_mcs *mcs = data;
const struct match_mcs_data *match = user_data;
if (match->db != mcs->db)
return false;
if (match->gmcs)
return mcs->ldb.gmcs;
if (match->any)
return true;
return match->ccid == mcs->ldb.ccid_value;
}
static int mcs_alloc_ccid(struct gatt_db *db)
{
unsigned int ccid;
if (!db)
return 0;
for (ccid = servers_ccid; ccid < servers_ccid + 0x100u; ccid++) {
struct match_mcs_data match = { .db = db, .ccid = ccid & 0xff };
if (!queue_find(servers, match_mcs, &match)) {
servers_ccid = ccid + 1;
return match.ccid;
}
}
return -ENOENT;
}
void bt_mcs_test_util_reset_ccid(void)
{
servers_ccid = 0;
}
struct bt_mcs *bt_mcs_register(struct gatt_db *db, bool is_gmcs,
const struct bt_mcs_callback *cb, void *user_data)
{
struct bt_mcs *mcs;
int ccid;
if (!db || !cb)
return NULL;
if (is_gmcs) {
struct match_mcs_data match = { .db = db, .gmcs = true };
/* Only one GMCS possible */
if (queue_find(servers, match_mcs, &match))
return NULL;
}
ccid = mcs_alloc_ccid(db);
if (ccid < 0)
return NULL;
mcs = new0(struct bt_mcs, 1);
mcs->db = db;
mcs->ldb.ccid_value = ccid;
mcs->cb = cb;
mcs->user_data = user_data;
mcs->media_state = BT_MCS_STATE_INACTIVE;
mcs->sessions = queue_new();
if (!mcs_init_db(mcs, is_gmcs)) {
free(mcs);
return NULL;
}
gatt_db_ref(mcs->db);
if (!servers)
servers = queue_new();
queue_push_tail(servers, mcs);
gatt_db_service_set_active(mcs->ldb.service, true);
return mcs;
}
void bt_mcs_unregister(struct bt_mcs *mcs)
{
if (!mcs)
return;
if (mcs->cb->destroy)
mcs->cb->destroy(mcs->user_data);
queue_remove(servers, mcs);
gatt_db_remove_service(mcs->db, mcs->ldb.service);
gatt_db_unref(mcs->db);
if (queue_isempty(servers)) {
queue_destroy(servers, NULL);
servers = NULL;
}
queue_destroy(mcs->sessions, session_remove);
free(mcs);
}
void bt_mcs_unregister_all(struct gatt_db *db)
{
struct bt_mcs *mcs;
do {
struct match_mcs_data match = { .db = db, .any = true };
mcs = queue_find(servers, match_mcs, &match);
bt_mcs_unregister(mcs);
} while (mcs);
}
/*
* MCP Client
*/
static void mcp_service_reread(struct bt_mcp_service *service,
struct gatt_db_attribute *attrib,
bool skip_notify);
static void foreach_mcs_char(struct gatt_db_attribute *attr, void *user_data);
static void mcp_debug_func(const char *str, void *user_data)
{
struct bt_mcp *mcp = user_data;
mcp->cb->debug(mcp->user_data, str);
}
static void mcp_debug(struct bt_mcp *mcp, const char *format, ...)
{
va_list ap;
if (!mcp || !format || !mcp->cb->debug)
return;
va_start(ap, format);
util_debug_va(mcp_debug_func, mcp, format, ap);
va_end(ap);
}
static bool match_ccid(const void *data, const void *user_data)
{
const struct bt_mcp_service *service = data;
return service->rdb.ccid_value == (int)PTR_TO_UINT(user_data);
}
static struct bt_mcp_service *mcp_service(struct bt_mcp *mcp, uint8_t ccid)
{
if (!mcp)
return NULL;
return queue_find(mcp->services, match_ccid, UINT_TO_PTR(ccid));
}
static bool match_pending(const void *data, const void *user_data)
{
const struct bt_mcp_pending *pending = data;
return pending->id == PTR_TO_UINT(user_data);
}
static struct bt_mcp_pending *mcp_pending_new(struct bt_mcp_service *service)
{
struct bt_mcp_pending *pending;
if (queue_length(service->pending) > MAX_PENDING)
return NULL;
while (!service->pending_id || queue_find(service->pending,
match_pending, UINT_TO_PTR(service->pending_id)))
service->pending_id++;
pending = new0(struct bt_mcp_pending, 1);
pending->service = service;
pending->id = service->pending_id++;
return pending;
}
static unsigned int mcp_send(struct bt_mcp_service *service, uint8_t *buf,
uint16_t length)
{
struct bt_mcp *mcp = service->mcp;
uint16_t handle;
struct bt_mcp_pending *pending;
int ret;
uint8_t op = buf[0];
if (!gatt_db_attribute_get_char_data(service->rdb.media_cp, NULL,
&handle, NULL, NULL, NULL))
return 0;
pending = mcp_pending_new(service);
if (!pending)
return 0;
ret = bt_gatt_client_write_without_response(mcp->client,
handle, false, buf, length);
if (!ret) {
free(pending);
return 0;
}
pending->op = op;
queue_push_tail(service->pending, pending);
DBG_SVC(service, "%u", pending->id);
return pending->id;
}
static void mcp_pending_write_cb(bool success, uint8_t att_ecode,
void *user_data)
{
struct bt_mcp_pending *pending = user_data;
if (!success) {
pending->write.result = BT_MCS_RESULT_COMMAND_CANNOT_COMPLETE;
return;
}
pending->write.result = BT_MCS_RESULT_SUCCESS;
/* If the attribute doesn't have notify, reread to get the new value */
mcp_service_reread(pending->service, pending->write.attrib, true);
}
static void mcp_pending_write_done(void *user_data)
{
struct bt_mcp_pending *pending = user_data;
struct bt_mcp_service *service = pending->service;
struct bt_mcp *mcp = service->mcp;
DBG_SVC(service, "write %u", pending->id);
queue_remove(service->pending, pending);
if (mcp->cb->complete)
mcp->cb->complete(mcp->user_data, pending->id,
pending->write.result);
free(pending);
}
static unsigned int mcp_write_chrc(struct bt_mcp_service *service,
struct gatt_db_attribute *attrib, void *data, uint16_t length)
{
struct bt_mcp *mcp;
struct bt_mcp_pending *pending;
uint16_t handle;
if (!service)
return 0;
mcp = service->mcp;
if (!gatt_db_attribute_get_char_data(attrib, NULL, &handle, NULL, NULL,
NULL))
return 0;
pending = mcp_pending_new(service);
if (!pending)
return 0;
pending->write.attrib = attrib;
pending->write.client_id = bt_gatt_client_write_value(mcp->client,
handle, data, length, mcp_pending_write_cb,
pending, mcp_pending_write_done);
if (!pending->write.client_id) {
free(pending);
return 0;
}
queue_push_tail(service->pending, pending);
return pending->id;
}
static bool match_pending_write(const void *data, const void *user_data)
{
const struct bt_mcp_pending *pending = data;
return !pending->op;
}
static void mcp_cancel_pending_writes(struct bt_mcp_service *service)
{
struct bt_mcp_pending *pending;
struct bt_gatt_client *client = service->mcp->client;
do {
pending = queue_remove_if(service->pending, match_pending_write,
NULL);
if (pending) {
if (!bt_gatt_client_cancel(client,
pending->write.client_id))
free(pending);
}
} while (pending);
}
static unsigned int mcp_command(struct bt_mcp *mcp, uint8_t ccid, uint8_t op,
int32_t arg)
{
const struct mcs_command *cmd = mcs_get_command(op);
struct bt_mcp_service *service = mcp_service(mcp, ccid);
uint8_t buf[5];
struct iovec iov = { .iov_base = buf, .iov_len = 0 };
if (!service || !cmd)
return 0;
if (!(service->rdb.media_cp_op_supported_value & cmd->support))
return 0;
DBG_SVC(service, "%s %d", cmd->name, arg);
util_iov_push_u8(&iov, op);
if (cmd->int32_arg)
util_iov_push_le32(&iov, arg);
return mcp_send(service, iov.iov_base, iov.iov_len);
}
unsigned int bt_mcp_play(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_PLAY, 0);
}
unsigned int bt_mcp_pause(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_PAUSE, 0);
}
unsigned int bt_mcp_fast_rewind(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_FAST_REWIND, 0);
}
unsigned int bt_mcp_fast_forward(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_FAST_FORWARD, 0);
}
unsigned int bt_mcp_stop(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_STOP, 0);
}
unsigned int bt_mcp_move_relative(struct bt_mcp *mcp, uint8_t ccid,
int32_t offset)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_MOVE_RELATIVE, offset);
}
unsigned int bt_mcp_previous_segment(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_PREV_SEGMENT, 0);
}
unsigned int bt_mcp_next_segment(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_NEXT_SEGMENT, 0);
}
unsigned int bt_mcp_first_segment(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_FIRST_SEGMENT, 0);
}
unsigned int bt_mcp_last_segment(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_LAST_SEGMENT, 0);
}
unsigned int bt_mcp_goto_segment(struct bt_mcp *mcp, uint8_t ccid, int32_t n)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_GOTO_SEGMENT, n);
}
unsigned int bt_mcp_previous_track(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_PREV_TRACK, 0);
}
unsigned int bt_mcp_next_track(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_NEXT_TRACK, 0);
}
unsigned int bt_mcp_first_track(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_FIRST_TRACK, 0);
}
unsigned int bt_mcp_last_track(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_LAST_TRACK, 0);
}
unsigned int bt_mcp_goto_track(struct bt_mcp *mcp, uint8_t ccid, int32_t n)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_GOTO_TRACK, n);
}
unsigned int bt_mcp_previous_group(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_PREV_GROUP, 0);
}
unsigned int bt_mcp_next_group(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_NEXT_GROUP, 0);
}
unsigned int bt_mcp_first_group(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_FIRST_GROUP, 0);
}
unsigned int bt_mcp_last_group(struct bt_mcp *mcp, uint8_t ccid)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_LAST_GROUP, 0);
}
unsigned int bt_mcp_goto_group(struct bt_mcp *mcp, uint8_t ccid, int32_t n)
{
return mcp_command(mcp, ccid, BT_MCS_CMD_GOTO_GROUP, n);
}
unsigned int bt_mcp_set_track_position(struct bt_mcp *mcp, uint8_t ccid,
int32_t position)
{
struct bt_mcp_service *service = mcp_service(mcp, ccid);
position = cpu_to_le32(position);
return mcp_write_chrc(service, service->rdb.track_position,
&position, sizeof(position));
}
unsigned int bt_mcp_set_playback_speed(struct bt_mcp *mcp, uint8_t ccid,
int8_t value)
{
struct bt_mcp_service *service = mcp_service(mcp, ccid);
return mcp_write_chrc(service, service->rdb.playback_speed,
&value, sizeof(value));
}
unsigned int bt_mcp_set_playing_order(struct bt_mcp *mcp, uint8_t ccid,
uint8_t value)
{
struct bt_mcp_service *service = mcp_service(mcp, ccid);
uint16_t support = 0;
unsigned int i;
if (!service)
return 0;
for (i = 0; i < ARRAY_SIZE(mcs_playing_orders); ++i) {
if (mcs_playing_orders[i].order == value) {
support = mcs_playing_orders[i].support;
break;
}
}
if (!(service->rdb.playing_order_supported_value & support))
return 0;
return mcp_write_chrc(service, service->rdb.playing_order,
&value, sizeof(value));
}
uint16_t bt_mcp_get_supported_playing_order(struct bt_mcp *mcp, uint8_t ccid)
{
struct bt_mcp_service *service = mcp_service(mcp, ccid);
if (!service)
return 0;
return service->rdb.playing_order_supported_value;
}
uint32_t bt_mcp_get_supported_commands(struct bt_mcp *mcp, uint8_t ccid)
{
struct bt_mcp_service *service = mcp_service(mcp, ccid);
if (!service)
return 0;
return service->rdb.media_cp_op_supported_value;
}
#define LISTENER_CB(service, method, ...) \
do { \
const struct queue_entry *entry = \
queue_get_entries((service)->listeners); \
for (; entry; entry = entry->next) { \
struct bt_mcp_listener *listener = entry->data; \
if (listener->cb->method) \
listener->cb->method(listener->user_data, \
## __VA_ARGS__); \
} \
} while (0)
static void update_media_player_name(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_mcp_service *service = user_data;
if (!success) {
DBG_SVC(service, "Unable to read Media Player Name: "
"error 0x%02x", att_ecode);
return;
}
DBG_SVC(service, "Media Player Name");
LISTENER_CB(service, media_player_name, value, length);
}
static void update_track_changed(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_mcp_service *service = user_data;
if (!success) {
DBG_SVC(service, "Unable to read Track Changed: "
"error 0x%02x", att_ecode);
return;
}
mcp_service_reread(service, NULL, true);
DBG_SVC(service, "Track Changed");
LISTENER_CB(service, track_changed);
}
static void update_track_title(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_mcp_service *service = user_data;
if (!success) {
DBG_SVC(service, "Unable to read Track Title: error 0x%02x",
att_ecode);
return;
}
DBG_SVC(service, "Track Title");
LISTENER_CB(service, track_title, value, length);
}
static void update_track_duration(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_mcp_service *service = user_data;
struct iovec iov = { .iov_base = (void *)value, .iov_len = length };
uint32_t v;
if (!success || !util_iov_pull_le32(&iov, &v)) {
DBG_SVC(service, "Unable to read Track Duration: "
"error 0x%02x", att_ecode);
return;
}
DBG_SVC(service, "Track Duration: %d", (int32_t)v);
LISTENER_CB(service, track_duration, (int32_t)v);
}
static void update_track_position(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_mcp_service *service = user_data;
struct iovec iov = { .iov_base = (void *)value, .iov_len = length };
uint32_t v;
if (!success || !util_iov_pull_le32(&iov, &v)) {
DBG_SVC(service, "Unable to read Track Position: "
"error 0x%02x", att_ecode);
return;
}
DBG_SVC(service, "Track Position: %d", (int32_t)v);
LISTENER_CB(service, track_position, (int32_t)v);
}
static void update_playback_speed(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_mcp_service *service = user_data;
struct iovec iov = { .iov_base = (void *)value, .iov_len = length };
uint8_t v;
if (!success || !util_iov_pull_u8(&iov, &v)) {
DBG_SVC(service, "Unable to read Playback Speed: "
"error 0x%02x", att_ecode);
return;
}
DBG_SVC(service, "Playback Speed: %d", (int8_t)v);
LISTENER_CB(service, playback_speed, (int8_t)v);
}
static void update_seeking_speed(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_mcp_service *service = user_data;
struct iovec iov = { .iov_base = (void *)value, .iov_len = length };
uint8_t v;
if (!success || !util_iov_pull_u8(&iov, &v)) {
DBG_SVC(service, "Unable to read Seeking Speed: "
"error 0x%02x", att_ecode);
return;
}
DBG_SVC(service, "Seeking Speed: %d", (int8_t)v);
LISTENER_CB(service, seeking_speed, (int8_t)v);
}
static void update_playing_order(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_mcp_service *service = user_data;
struct iovec iov = { .iov_base = (void *)value, .iov_len = length };
uint8_t v;
if (!success || !util_iov_pull_u8(&iov, &v)) {
DBG_SVC(service, "Unable to read Playing Order: "
"error 0x%02x", att_ecode);
return;
}
DBG_SVC(service, "Playing Order: %u", v);
LISTENER_CB(service, playing_order, v);
}
static void update_playing_order_supported(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_mcp_service *service = user_data;
struct iovec iov = { .iov_base = (void *)value, .iov_len = length };
uint16_t v;
if (!success || !util_iov_pull_le16(&iov, &v)) {
DBG_SVC(service, "Unable to read "
"Playing Order Supported: error 0x%02x", att_ecode);
return;
}
DBG_SVC(service, "Playing Order Supported: %u", v);
service->rdb.playing_order_supported_value = v;
}
static void update_media_state(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_mcp_service *service = user_data;
struct iovec iov = { .iov_base = (void *)value, .iov_len = length };
uint8_t v;
if (!success || !util_iov_pull_u8(&iov, &v)) {
DBG_SVC(service, "Unable to read Media State: error 0x%02x",
att_ecode);
return;
}
DBG_SVC(service, "Media State: %u", v);
LISTENER_CB(service, media_state, v);
}
static bool match_pending_op(const void *data, const void *user_data)
{
const struct bt_mcp_pending *pending = data;
return pending->op && pending->op == PTR_TO_UINT(user_data);
}
static void update_media_cp(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_mcp_service *service = user_data;
struct bt_mcp *mcp = service->mcp;
struct iovec iov = { .iov_base = (void *)value, .iov_len = length };
struct bt_mcp_pending *pending;
uint8_t op, result;
if (!success || !util_iov_pull_u8(&iov, &op) ||
!util_iov_pull_u8(&iov, &result)) {
DBG_SVC(service, "Unable to read Media CP: error 0x%02x",
att_ecode);
return;
}
DBG_SVC(service, "Media CP %u result %u", op, result);
pending = queue_remove_if(service->pending, match_pending_op,
UINT_TO_PTR(op));
if (!pending)
return;
if (mcp->cb->complete)
mcp->cb->complete(mcp->user_data, pending->id, result);
free(pending);
}
static void update_media_cp_op_supported(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_mcp_service *service = user_data;
struct iovec iov = { .iov_base = (void *)value, .iov_len = length };
uint32_t v;
if (!success || !util_iov_pull_le32(&iov, &v)) {
DBG_SVC(service, "Unable to read "
"Media CP Op Supported: error 0x%02x", att_ecode);
return;
}
DBG_SVC(service, "Media CP Op Supported: %d", v);
service->rdb.media_cp_op_supported_value = v;
}
static void update_add_service(void *data, void *user_data)
{
struct bt_mcp_service *service = data;
struct bt_mcp *mcp = user_data;
if (service->rdb.ccid_value < 0)
return;
if (service->ready)
return;
service->ready = true;
if (mcp->cb->ccid)
mcp->cb->ccid(mcp->user_data, service->rdb.ccid_value,
service->rdb.gmcs);
}
static void update_ccid(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_mcp_service *service = user_data;
struct iovec iov = { .iov_base = (void *)value, .iov_len = length };
uint8_t v;
if (!success || !util_iov_pull_u8(&iov, &v)) {
DBG_SVC(service, "Unable to read CCID: error 0x%02x",
att_ecode);
return;
}
DBG_SVC(service, "CCID: %u", v);
service->rdb.ccid_value = v;
gatt_db_service_foreach_char(service->rdb.service, foreach_mcs_char,
service);
update_add_service(service, service->mcp);
}
static void mcp_service_reread(struct bt_mcp_service *service,
struct gatt_db_attribute *attrib,
bool skip_notify)
{
const struct {
struct gatt_db_attribute *attr;
bt_gatt_client_read_callback_t cb;
} attrs[] = {
{ service->rdb.track_title, update_track_title },
{ service->rdb.track_duration, update_track_duration },
{ service->rdb.track_position, update_track_position },
{ service->rdb.playback_speed, update_playback_speed },
{ service->rdb.seeking_speed, update_seeking_speed },
{ service->rdb.playing_order, update_playing_order },
{ service->rdb.playing_order_supported,
update_playing_order_supported },
{ service->rdb.media_state, update_media_state },
{ service->rdb.media_cp_op_supported,
update_media_cp_op_supported },
};
struct bt_gatt_client *client = service->mcp->client;
uint16_t value_handle;
uint8_t props;
unsigned int i;
for (i = 0; i < ARRAY_SIZE(attrs); ++i) {
if (!attrs[i].attr)
continue;
if (attrib && attrs[i].attr != attrib)
continue;
if (!gatt_db_attribute_get_char_data(attrs[i].attr, NULL,
&value_handle, &props, NULL, NULL))
continue;
if (skip_notify && (props & BT_GATT_CHRC_PROP_NOTIFY))
continue;
DBG_SVC(service, "re-read handle 0x%04x", value_handle);
bt_gatt_client_read_value(client, value_handle,
attrs[i].cb, service, NULL);
}
}
static void notify_media_player_name(struct bt_mcp_service *service)
{
/* On player name change, re-read all attributes */
mcp_service_reread(service, NULL, false);
}
static void mcp_idle(void *data)
{
struct bt_mcp *mcp = data;
DBG_MCP(mcp, "");
mcp->idle_id = 0;
if (!mcp->ready) {
mcp->ready = true;
if (mcp->cb->ready)
mcp->cb->ready(mcp->user_data);
}
}
struct chrc_notify_data {
const char *name;
struct bt_mcp_service *service;
bt_gatt_client_read_callback_t cb;
void (*notify_cb)(struct bt_mcp_service *service);
};
static void chrc_register(uint16_t att_ecode, void *user_data)
{
struct chrc_notify_data *data = user_data;
if (att_ecode)
DBG_SVC(data->service, "%s notification failed: 0x%04x",
data->name, att_ecode);
}
static void chrc_notify(uint16_t value_handle, const uint8_t *value,
uint16_t length, void *user_data)
{
struct chrc_notify_data *data = user_data;
struct bt_mcp_service *service = data->service;
struct bt_gatt_client *client = service->mcp->client;
uint16_t mtu = bt_gatt_client_get_mtu(client);
DBG_SVC(service, "Notify %s", data->name);
if (length == mtu - 3) {
/* Probably truncated value */
DBG_SVC(service, "Read %s", data->name);
bt_gatt_client_read_value(client, value_handle,
data->cb, service, NULL);
return;
}
data->cb(true, 0xff, value, length, data->service);
if (data->notify_cb)
data->notify_cb(service);
}
static void foreach_mcs_char(struct gatt_db_attribute *attr, void *user_data)
{
struct bt_mcp_service *service = user_data;
struct bt_mcp *mcp = service->mcp;
const struct {
uint16_t uuid;
const char *name;
struct gatt_db_attribute **dst;
bt_gatt_client_read_callback_t cb;
void (*notify_cb)(struct bt_mcp_service *service);
bool no_read;
bool no_notify;
} attrs[] = {
{ MCS_CCID_CHRC_UUID, "CCID", &service->rdb.ccid,
update_ccid, .no_notify = true },
{ MCS_MEDIA_PLAYER_NAME_CHRC_UUID, "Media Player Name",
&service->rdb.media_player_name, update_media_player_name,
.notify_cb = notify_media_player_name },
{ MCS_TRACK_CHANGED_CHRC_UUID, "Track Changed",
&service->rdb.track_changed, update_track_changed,
.no_read = true },
{ MCS_TRACK_TITLE_CHRC_UUID, "Track Title",
&service->rdb.track_title, update_track_title },
{ MCS_TRACK_DURATION_CHRC_UUID, "Track Duration",
&service->rdb.track_duration, update_track_duration },
{ MCS_TRACK_POSITION_CHRC_UUID, "Track Position",
&service->rdb.track_position, update_track_position },
{ MCS_PLAYBACK_SPEED_CHRC_UUID, "Playback Speed",
&service->rdb.playback_speed, update_playback_speed },
{ MCS_SEEKING_SPEED_CHRC_UUID, "Seeking Speed",
&service->rdb.seeking_speed, update_seeking_speed },
{ MCS_PLAYING_ORDER_CHRC_UUID, "Playing Order",
&service->rdb.playing_order, update_playing_order },
{ MCS_PLAYING_ORDER_SUPPORTED_CHRC_UUID,
"Playing Order Supported",
&service->rdb.playing_order_supported,
update_playing_order_supported, .no_notify = true },
{ MCS_MEDIA_STATE_CHRC_UUID, "Media State",
&service->rdb.media_state, update_media_state },
{ MCS_MEDIA_CP_CHRC_UUID, "Media Control Point",
&service->rdb.media_cp, update_media_cp },
{ MCS_MEDIA_CP_OP_SUPPORTED_CHRC_UUID, "Media CP Op Supported",
&service->rdb.media_cp_op_supported,
update_media_cp_op_supported },
};
struct bt_gatt_client *client = service->mcp->client;
bt_uuid_t uuid, uuid_attr;
uint16_t value_handle;
uint8_t props;
unsigned int i;
if (!gatt_db_attribute_get_char_data(attr, NULL, &value_handle,
&props, NULL, &uuid_attr))
return;
for (i = 0; i < ARRAY_SIZE(attrs); ++i) {
unsigned int id;
struct chrc_notify_data *data;
if (*attrs[i].dst)
continue;
bt_uuid16_create(&uuid, attrs[i].uuid);
if (bt_uuid_cmp(&uuid_attr, &uuid))
continue;
DBG_SVC(service, "%s found: handle 0x%04x",
attrs[i].name, value_handle);
*attrs[i].dst = attr;
if ((props & BT_GATT_CHRC_PROP_READ) && !attrs[i].no_read)
bt_gatt_client_read_value(client, value_handle,
attrs[i].cb, service, NULL);
if (!(props & BT_GATT_CHRC_PROP_NOTIFY) || attrs[i].no_notify)
break;
if (service->notify_id_count >= ARRAY_SIZE(service->notify_id))
break;
data = new0(struct chrc_notify_data, 1);
data->name = attrs[i].name;
data->service = service;
data->cb = attrs[i].cb;
id = bt_gatt_client_register_notify(client, value_handle,
chrc_register, chrc_notify,
data, free);
if (id)
service->notify_id[service->notify_id_count++] = id;
else
free(data);
break;
}
if (!mcp->idle_id && i < ARRAY_SIZE(attrs))
mcp->idle_id = bt_gatt_client_idle_register(mcp->client,
mcp_idle, mcp, NULL);
}
static void foreach_mcs_ccid(struct gatt_db_attribute *attr, void *user_data)
{
bt_uuid_t uuid, uuid_attr;
if (!gatt_db_attribute_get_char_data(attr, NULL, NULL, NULL, NULL,
&uuid_attr))
return;
bt_uuid16_create(&uuid, MCS_CCID_CHRC_UUID);
if (bt_uuid_cmp(&uuid_attr, &uuid))
return;
foreach_mcs_char(attr, user_data);
}
static void listener_destroy(void *data)
{
struct bt_mcp_listener *listener = data;
if (listener->cb->destroy)
listener->cb->destroy(listener->user_data);
free(listener);
}
static void mcp_service_destroy(void *data)
{
struct bt_mcp_service *service = data;
struct bt_gatt_client *client = service->mcp->client;
unsigned int i;
mcp_cancel_pending_writes(service);
queue_destroy(service->listeners, listener_destroy);
for (i = 0; i < service->notify_id_count; ++i)
bt_gatt_client_unregister_notify(client, service->notify_id[i]);
queue_destroy(service->pending, free);
free(service);
}
static void foreach_mcs_service(struct gatt_db_attribute *attr, void *user_data)
{
struct bt_mcp *mcp = user_data;
struct bt_mcp_service *service;
bt_uuid_t uuid, uuid_attr;
bool gmcs, mcs;
DBG_MCP(mcp, "");
if (!gatt_db_attribute_get_service_uuid(attr, &uuid_attr))
return;
bt_uuid16_create(&uuid, GMCS_UUID);
gmcs = !bt_uuid_cmp(&uuid_attr, &uuid);
if (gmcs != mcp->gmcs)
return;
bt_uuid16_create(&uuid, MCS_UUID);
mcs = !bt_uuid_cmp(&uuid_attr, &uuid);
if (!gmcs && !mcs)
return;
service = new0(struct bt_mcp_service, 1);
service->mcp = mcp;
service->rdb.gmcs = gmcs;
service->rdb.service = attr;
service->rdb.ccid_value = -1;
service->pending = queue_new();
service->listeners = queue_new();
/* Find CCID first */
gatt_db_service_foreach_char(attr, foreach_mcs_ccid, service);
queue_push_tail(mcp->services, service);
}
static bool match_service_attr(const void *data, const void *user_data)
{
const struct bt_mcp_service *service = data;
return service->rdb.service == user_data;
}
static void mcp_service_added(struct gatt_db_attribute *attr, void *user_data)
{
struct bt_mcp *mcp = user_data;
foreach_mcs_service(attr, mcp);
}
static void mcp_service_removed(struct gatt_db_attribute *attr, void *user_data)
{
struct bt_mcp *mcp = user_data;
queue_remove_all(mcp->services, match_service_attr, attr,
mcp_service_destroy);
}
struct bt_mcp *bt_mcp_attach(struct bt_gatt_client *client, bool gmcs,
const struct bt_mcp_callback *cb, void *user_data)
{
struct bt_mcp *mcp;
struct gatt_db *db;
bt_uuid_t uuid;
if (!cb)
return NULL;
client = bt_gatt_client_clone(client);
if (!client)
return NULL;
mcp = new0(struct bt_mcp, 1);
mcp->gmcs = gmcs;
mcp->client = client;
mcp->services = queue_new();
mcp->cb = cb;
mcp->user_data = user_data;
DBG_MCP(mcp, "");
db = bt_gatt_client_get_db(client);
bt_uuid16_create(&uuid, GMCS_UUID);
gatt_db_foreach_service(db, &uuid, foreach_mcs_service, mcp);
bt_uuid16_create(&uuid, MCS_UUID);
gatt_db_foreach_service(db, &uuid, foreach_mcs_service, mcp);
mcp->db_id = gatt_db_register(db, mcp_service_added,
mcp_service_removed, mcp, NULL);
if (!mcp->idle_id)
mcp_idle(mcp);
return mcp;
}
void bt_mcp_detach(struct bt_mcp *mcp)
{
struct gatt_db *db;
if (!mcp)
return;
DBG_MCP(mcp, "");
queue_destroy(mcp->services, mcp_service_destroy);
if (mcp->cb->destroy)
mcp->cb->destroy(mcp->user_data);
if (mcp->idle_id)
bt_gatt_client_idle_unregister(mcp->client, mcp->idle_id);
db = bt_gatt_client_get_db(mcp->client);
if (mcp->db_id)
gatt_db_unregister(db, mcp->db_id);
bt_gatt_client_unref(mcp->client);
free(mcp);
}
bool bt_mcp_add_listener(struct bt_mcp *mcp, uint8_t ccid,
const struct bt_mcp_listener_callback *cb,
void *user_data)
{
struct bt_mcp_listener *listener;
struct bt_mcp_service *service;
if (!cb)
return false;
service = queue_find(mcp->services, match_ccid, UINT_TO_PTR(ccid));
if (!service)
return false;
listener = new0(struct bt_mcp_listener, 1);
listener->cb = cb;
listener->user_data = user_data;
queue_push_tail(service->listeners, listener);
return true;
}
struct bt_gatt_client *bt_mcp_test_util_get_client(struct bt_mcp *mcp)
{
return mcp->client;
}