/*
|
* BlueALSA - ctl-client.c
|
* Copyright (c) 2016-2018 Arkadiusz Bokowy
|
*
|
* This file is a part of bluez-alsa.
|
*
|
* This project is licensed under the terms of the MIT license.
|
*
|
*/
|
|
#include "shared/ctl-client.h"
|
|
#include <errno.h>
|
#include <fcntl.h>
|
#include <stdlib.h>
|
#include <unistd.h>
|
#include <sys/socket.h>
|
#include <sys/stat.h>
|
#include <sys/types.h>
|
#include <sys/un.h>
|
|
#include "shared/log.h"
|
|
|
/**
|
* Convert BlueALSA status message into the POSIX errno value. */
|
static int bluealsa_status_to_errno(const struct ba_msg_status *status) {
|
switch (status->code) {
|
case BA_STATUS_CODE_SUCCESS:
|
return 0;
|
case BA_STATUS_CODE_ERROR_UNKNOWN:
|
return EIO;
|
case BA_STATUS_CODE_DEVICE_NOT_FOUND:
|
return ENODEV;
|
case BA_STATUS_CODE_STREAM_NOT_FOUND:
|
return ENXIO;
|
case BA_STATUS_CODE_DEVICE_BUSY:
|
return EBUSY;
|
case BA_STATUS_CODE_FORBIDDEN:
|
return EACCES;
|
default:
|
/* some generic error code */
|
return EINVAL;
|
}
|
}
|
|
#if DEBUG
|
/**
|
* Convert Bluetooth address into a human-readable string.
|
*
|
* In order to convert Bluetooth address into the human-readable string, one
|
* might use the ba2str() from the bluetooth library. However, it would be the
|
* only used function from this library. In order to avoid excessive linking,
|
* we are providing our own implementation of this function.
|
*
|
* @param ba Pointer to the Bluetooth address structure.
|
* @param str Pointer to buffer big enough to contain string representation
|
* of the Bluetooth address.
|
* @return Pointer to the destination string str. */
|
static char *ba2str_(const bdaddr_t *ba, char str[18]) {
|
sprintf(str, "%2.2X:%2.2X:%2.2X:%2.2X:%2.2X:%2.2X",
|
ba->b[5], ba->b[4], ba->b[3], ba->b[2], ba->b[1], ba->b[0]);
|
return str;
|
}
|
#endif
|
|
/**
|
* Send request to the BlueALSA server.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param req An address to the request structure.
|
* @return Upon success this function returns 0. Otherwise, -1 is returned
|
* and errno is set appropriately. */
|
static int bluealsa_send_request(int fd, const struct ba_request *req) {
|
|
struct ba_msg_status status = { 0xAB };
|
|
if (send(fd, req, sizeof(*req), MSG_NOSIGNAL) == -1)
|
return -1;
|
if (read(fd, &status, sizeof(status)) == -1)
|
return -1;
|
|
errno = bluealsa_status_to_errno(&status);
|
return errno != 0 ? -1 : 0;
|
}
|
|
/**
|
* Open BlueALSA connection.
|
*
|
* @param interface HCI interface to use.
|
* @return On success this function returns socket file descriptor. Otherwise,
|
* -1 is returned and errno is set to indicate the error. */
|
int bluealsa_open(const char *interface) {
|
|
const uint16_t ver = BLUEALSA_CRL_PROTO_VERSION;
|
int fd, err;
|
|
struct sockaddr_un saddr = { .sun_family = AF_UNIX };
|
snprintf(saddr.sun_path, sizeof(saddr.sun_path) - 1,
|
BLUEALSA_RUN_STATE_DIR "/%s", interface);
|
|
if ((fd = socket(PF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0)) == -1)
|
return -1;
|
|
debug("Connecting to socket: %s", saddr.sun_path);
|
if (connect(fd, (struct sockaddr *)(&saddr), sizeof(saddr)) == -1) {
|
err = errno;
|
close(fd);
|
errno = err;
|
return -1;
|
}
|
|
if (send(fd, &ver, sizeof(ver), MSG_NOSIGNAL) == -1)
|
return -1;
|
|
return fd;
|
}
|
|
/**
|
* Subscribe for notifications.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param mask Bit-mask with events for which client wants to be subscribed.
|
* In order to cancel subscription, use empty event mask.
|
* @return Upon success this function returns 0. Otherwise, -1 is returned
|
* and errno is set appropriately. */
|
int bluealsa_subscribe(int fd, enum ba_event mask) {
|
const struct ba_request req = {
|
.command = BA_COMMAND_SUBSCRIBE,
|
.events = mask,
|
};
|
debug("Subscribing for events: %B", mask);
|
return bluealsa_send_request(fd, &req);
|
}
|
|
/**
|
* Get the list of connected Bluetooth devices.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param devices An address where the device list will be stored.
|
* @return Upon success this function returns the number of connected devices
|
* and the `devices` address is modified to point to the devices list array,
|
* which should be freed with the free(). On error, -1 is returned and errno
|
* is set to indicate the error. */
|
ssize_t bluealsa_get_devices(int fd, struct ba_msg_device **devices) {
|
|
const struct ba_request req = { .command = BA_COMMAND_LIST_DEVICES };
|
struct ba_msg_device *_devices = NULL;
|
struct ba_msg_device device;
|
size_t i = 0;
|
|
if (send(fd, &req, sizeof(req), MSG_NOSIGNAL) == -1)
|
return -1;
|
|
while (recv(fd, &device, sizeof(device), 0) == sizeof(device)) {
|
_devices = realloc(_devices, (i + 1) * sizeof(*_devices));
|
memcpy(&_devices[i], &device, sizeof(*_devices));
|
i++;
|
}
|
|
*devices = _devices;
|
return i;
|
}
|
|
/**
|
* Get the list of available PCM transports.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param transports An address where the transport list will be stored.
|
* @return Upon success this function returns the number of available PCM
|
* transports and the `transports` address is modified to point to the
|
* transport list array, which should be freed with the free(). On error,
|
* -1 is returned and errno is set to indicate the error. */
|
ssize_t bluealsa_get_transports(int fd, struct ba_msg_transport **transports) {
|
|
const struct ba_request req = { .command = BA_COMMAND_LIST_TRANSPORTS };
|
struct ba_msg_transport *_transports = NULL;
|
struct ba_msg_transport transport;
|
size_t i = 0;
|
|
if (send(fd, &req, sizeof(req), MSG_NOSIGNAL) == -1)
|
return -1;
|
|
while (recv(fd, &transport, sizeof(transport), 0) == sizeof(transport)) {
|
_transports = realloc(_transports, (i + 1) * sizeof(*_transports));
|
memcpy(&_transports[i], &transport, sizeof(*_transports));
|
i++;
|
}
|
|
*transports = _transports;
|
return i;
|
}
|
|
/**
|
* Get PCM transport.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param addr MAC address of the Bluetooth device.
|
* @param type PCM type to get.
|
* @param stream Stream direction to get, e.g. playback or capture.
|
* @param transport An address where the transport will be stored.
|
* @return Upon success this function returns 0. Otherwise, -1 is returned
|
* and errno is set appropriately. */
|
int bluealsa_get_transport(int fd, bdaddr_t addr, enum ba_pcm_type type,
|
enum ba_pcm_stream stream, struct ba_msg_transport *transport) {
|
|
struct ba_msg_status status = { 0xAB };
|
struct ba_request req = {
|
.command = BA_COMMAND_TRANSPORT_GET,
|
.addr = addr,
|
.type = type,
|
.stream = stream,
|
};
|
ssize_t len;
|
|
#if DEBUG
|
char addr_[18];
|
ba2str_(&req.addr, addr_);
|
debug("Getting transport for %s type %d", addr_, type);
|
#endif
|
|
if (send(fd, &req, sizeof(req), MSG_NOSIGNAL) == -1)
|
return -1;
|
if ((len = read(fd, transport, sizeof(*transport))) == -1)
|
return -1;
|
|
/* in case of error, status message is returned */
|
if (len != sizeof(*transport)) {
|
memcpy(&status, transport, sizeof(status));
|
errno = bluealsa_status_to_errno(&status);
|
return -1;
|
}
|
|
if (read(fd, &status, sizeof(status)) == -1)
|
return -1;
|
|
return 0;
|
}
|
|
/**
|
* Get PCM transport delay.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param transport Address to the transport structure with the addr, type
|
* and stream fields set - other fields are not used by this function.
|
* @param delay An address where the transport delay will be stored.
|
* @return Upon success this function returns 0. Otherwise, -1 is returned
|
* and errno is set appropriately. */
|
int bluealsa_get_transport_delay(int fd, const struct ba_msg_transport *transport,
|
unsigned int *delay) {
|
|
struct ba_msg_transport t;
|
int ret;
|
|
if ((ret = bluealsa_get_transport(fd, transport->addr,
|
transport->type, transport->stream, &t)) == 0)
|
*delay = t.delay;
|
|
return ret;
|
}
|
|
/**
|
* Set PCM transport delay.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param transport Address to the transport structure with the addr, type
|
* and stream fields set - other fields are not used by this function.
|
* @param delay Transport delay.
|
* @return Upon success this function returns 0. Otherwise, -1 is returned
|
* and errno is set appropriately. */
|
int bluealsa_set_transport_delay(int fd, const struct ba_msg_transport *transport,
|
unsigned int delay) {
|
|
struct ba_request req = {
|
.command = BA_COMMAND_TRANSPORT_SET_DELAY,
|
.addr = transport->addr,
|
.type = transport->type,
|
.stream = transport->stream,
|
.delay = delay,
|
};
|
|
return bluealsa_send_request(fd, &req);
|
}
|
|
/**
|
* Get PCM transport volume.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param transport Address to the transport structure with the addr, type
|
* and stream fields set - other fields are not used by this function.
|
* @param ch1_muted An address where the mute of channel 1 will be stored.
|
* @param ch1_volume An address where the volume of channel 1 will be stored.
|
* @param ch2_muted An address where the mute of channel 2 will be stored.
|
* @param ch2_volume An address where the volume of channel 2 will be stored.
|
* @return Upon success this function returns 0. Otherwise, -1 is returned
|
* and errno is set appropriately. */
|
int bluealsa_get_transport_volume(int fd, const struct ba_msg_transport *transport,
|
bool *ch1_muted, int *ch1_volume, bool *ch2_muted, int *ch2_volume) {
|
|
struct ba_msg_transport t;
|
int ret;
|
|
if ((ret = bluealsa_get_transport(fd, transport->addr,
|
transport->type, transport->stream, &t)) == 0) {
|
*ch1_muted = t.ch1_muted;
|
*ch1_volume = t.ch1_volume;
|
*ch2_muted = t.ch2_muted;
|
*ch2_volume = t.ch2_volume;
|
}
|
|
return ret;
|
}
|
|
/**
|
* Set PCM transport volume.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param transport Address to the transport structure with the addr, type
|
* and stream fields set - other fields are not used by this function.
|
* @param ch1_muted If true, mute channel 1.
|
* @param ch1_volume Channel 1 volume in range [0, 127].
|
* @param ch2_muted If true, mute channel 2.
|
* @param ch2_volume Channel 2 volume in range [0, 127].
|
* @return Upon success this function returns 0. Otherwise, -1 is returned
|
* and errno is set appropriately. */
|
int bluealsa_set_transport_volume(int fd, const struct ba_msg_transport *transport,
|
bool ch1_muted, int ch1_volume, bool ch2_muted, int ch2_volume) {
|
|
struct ba_request req = {
|
.command = BA_COMMAND_TRANSPORT_SET_VOLUME,
|
.addr = transport->addr,
|
.type = transport->type,
|
.stream = transport->stream,
|
.ch1_muted = ch1_muted,
|
.ch1_volume = ch1_volume,
|
.ch2_muted = ch2_muted,
|
.ch2_volume = ch2_volume,
|
};
|
|
return bluealsa_send_request(fd, &req);
|
}
|
|
/**
|
* Open PCM transport.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param transport Address to the transport structure with the addr, type
|
* and stream fields set - other fields are not used by this function.
|
* @return PCM FIFO file descriptor, or -1 on error. */
|
int bluealsa_open_transport(int fd, const struct ba_msg_transport *transport) {
|
|
struct ba_msg_status status = { 0xAB };
|
struct ba_request req = {
|
.command = BA_COMMAND_PCM_OPEN,
|
.addr = transport->addr,
|
.type = transport->type,
|
.stream = transport->stream,
|
};
|
char buf[256] = "";
|
struct iovec io = {
|
.iov_base = &status,
|
.iov_len = sizeof(status),
|
};
|
struct msghdr msg = {
|
.msg_iov = &io,
|
.msg_iovlen = 1,
|
.msg_control = buf,
|
.msg_controllen = sizeof(buf),
|
};
|
ssize_t len;
|
|
#if DEBUG
|
char addr_[18];
|
ba2str_(&req.addr, addr_);
|
debug("Requesting PCM open for %s", addr_);
|
#endif
|
|
if (send(fd, &req, sizeof(req), MSG_NOSIGNAL) == -1)
|
return -1;
|
if ((len = recvmsg(fd, &msg, MSG_CMSG_CLOEXEC)) == -1)
|
return -1;
|
|
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
|
if (cmsg == NULL ||
|
cmsg->cmsg_level == IPPROTO_IP ||
|
cmsg->cmsg_type == IP_TTL) {
|
/* in case of error, status message is returned */
|
errno = bluealsa_status_to_errno(&status);
|
return -1;
|
}
|
|
if (read(fd, &status, sizeof(status)) == -1)
|
return -1;
|
|
return *((int *)CMSG_DATA(cmsg));
|
}
|
|
/**
|
* Close PCM transport.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param transport Address to the transport structure with the addr, type
|
* and stream fields set - other fields are not used by this function.
|
* @return Upon success this function returns 0. Otherwise, -1 is returned. */
|
int bluealsa_close_transport(int fd, const struct ba_msg_transport *transport) {
|
|
struct ba_request req = {
|
.command = BA_COMMAND_PCM_CLOSE,
|
.addr = transport->addr,
|
.type = transport->type,
|
.stream = transport->stream,
|
};
|
|
#if DEBUG
|
char addr_[18];
|
ba2str_(&req.addr, addr_);
|
debug("Closing PCM for %s", addr_);
|
#endif
|
|
return bluealsa_send_request(fd, &req);
|
}
|
|
/**
|
* Pause/resume PCM transport.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param transport Address to the transport structure with the addr, type
|
* and stream fields set - other fields are not used by this function.
|
* @param pause If non-zero, pause transport, otherwise resume it.
|
* @return Upon success this function returns 0. Otherwise, -1 is returned. */
|
int bluealsa_pause_transport(int fd, const struct ba_msg_transport *transport, bool pause) {
|
|
struct ba_request req = {
|
.command = pause ? BA_COMMAND_PCM_PAUSE : BA_COMMAND_PCM_RESUME,
|
.addr = transport->addr,
|
.type = transport->type,
|
.stream = transport->stream,
|
};
|
|
#if DEBUG
|
char addr_[18];
|
ba2str_(&req.addr, addr_);
|
debug("Requesting PCM %s for %s", pause ? "pause" : "resume", addr_);
|
#endif
|
|
return bluealsa_send_request(fd, &req);
|
}
|
|
/**
|
* Drain PCM transport.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param transport Address to the transport structure with the addr, type
|
* and stream fields set - other fields are not used by this function.
|
* @return Upon success this function returns 0. Otherwise, -1 is returned. */
|
int bluealsa_drain_transport(int fd, const struct ba_msg_transport *transport) {
|
|
struct ba_request req = {
|
.command = BA_COMMAND_PCM_DRAIN,
|
.addr = transport->addr,
|
.type = transport->type,
|
.stream = transport->stream,
|
};
|
|
#if DEBUG
|
char addr_[18];
|
ba2str_(&req.addr, addr_);
|
debug("Requesting PCM drain for %s", addr_);
|
#endif
|
|
return bluealsa_send_request(fd, &req);
|
}
|
|
/**
|
* Send RFCOMM message.
|
*
|
* @param fd Opened socket file descriptor.
|
* @param addr MAC address of the Bluetooth device.
|
* @param command NULL-terminated command string.
|
* @return Upon success this function returns 0. Otherwise, -1 is returned. */
|
int bluealsa_send_rfcomm_command(int fd, bdaddr_t addr, const char *command) {
|
|
struct ba_request req = {
|
.command = BA_COMMAND_RFCOMM_SEND,
|
.addr = addr,
|
};
|
|
/* snprintf() guarantees terminating NULL character */
|
snprintf(req.rfcomm_command, sizeof(req.rfcomm_command), "%s", command);
|
|
#if DEBUG
|
char addr_[18];
|
ba2str_(&req.addr, addr_);
|
debug("Sending RFCOMM command to %s: %s", addr_, req.rfcomm_command);
|
#endif
|
|
return bluealsa_send_request(fd, &req);
|
}
|