/* Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include <sys/socket.h>
#include <syslog.h>
#include "cras_bt_device.h"
#include "cras_telephony.h"
#include "cras_hfp_ag_profile.h"
#include "cras_hfp_slc.h"
#include "cras_system_state.h"
#define SLC_BUF_SIZE_BYTES 256
/* Indicator update command response and indicator indices.
* Note that indicator index starts from '1'.
*/
#define BATTERY_IND_INDEX 1
#define SIGNAL_IND_INDEX 2
#define SERVICE_IND_INDEX 3
#define CALL_IND_INDEX 4
#define CALLSETUP_IND_INDEX 5
#define CALLHELD_IND_INDEX 6
#define INDICATOR_UPDATE_RSP \
"+CIND: " \
"(\"battchg\",(0-5))," \
"(\"signal\",(0-5))," \
"(\"service\",(0,1))," \
"(\"call\",(0,1))," \
"(\"callsetup\",(0-3))," \
"(\"callheld\",(0-2))," \
"(\"roam\",(0,1))" \
""
/* Mode values for standard event reporting activation/deactivation AT
* command AT+CMER. Used for indicator events reporting in HFP. */
#define FORWARD_UNSOLICIT_RESULT_CODE 3
/* Handle object to hold required info to initialize and maintain
* an HFP service level connection.
* Args:
* buf - Buffer hold received commands.
* buf_read_idx - Read index for buf.
* buf_write_idx - Write index for buf.
* rfcomm_fd - File descriptor for the established RFCOMM connection.
* init_cb - Callback to be triggered when an SLC is initialized.
* initialized - The service level connection is fully initilized of not.
* cli_active - Calling line identification notification is enabled or not.
* battery - Current battery level of AG stored in SLC.
* signal - Current signal strength of AG stored in SLC.
* service - Current service availability of AG stored in SLC.
* callheld - Current callheld status of AG stored in SLC.
* ind_event_report - Activate status of indicator events reporting.
* telephony - A reference of current telephony handle.
* device - The associated bt device.
*/
struct hfp_slc_handle {
char buf[SLC_BUF_SIZE_BYTES];
int buf_read_idx;
int buf_write_idx;
int is_hsp;
int rfcomm_fd;
hfp_slc_init_cb init_cb;
hfp_slc_disconnect_cb disconnect_cb;
int initialized;
int cli_active;
int battery;
int signal;
int service;
int callheld;
int ind_event_report;
struct cras_bt_device *device;
struct cras_telephony_handle *telephony;
};
/* AT command exchanges between AG(Audio gateway) and HF(Hands-free device) */
struct at_command {
const char *cmd;
int (*callback) (struct hfp_slc_handle *handle, const char *cmd);
};
/* Sends a response or command to HF */
static int hfp_send(struct hfp_slc_handle *handle, const char *buf)
{
int written, err, len;
if (handle->rfcomm_fd < 0)
return -EIO;
/* Message start and end with "\r\n". refer to spec 4.33. */
err = write(handle->rfcomm_fd, "\r\n", 2);
if (err < 0)
return -errno;
len = strlen(buf);
written = 0;
while (written < len) {
err = write(handle->rfcomm_fd,
buf + written, len - written);
if (err < 0)
return -errno;
written += err;
}
err = write(handle->rfcomm_fd, "\r\n", 2);
if (err < 0)
return -errno;
return 0;
}
/* Sends a response for indicator event reporting. */
static int hfp_send_ind_event_report(struct hfp_slc_handle *handle,
int ind_index,
int value)
{
char cmd[64];
if (handle->is_hsp || !handle->ind_event_report)
return 0;
snprintf(cmd, 64, "+CIEV: %d,%d", ind_index, value);
return hfp_send(handle, cmd);
}
/* Sends calling line identification unsolicited result code and
* standard call waiting notification. */
static int hfp_send_calling_line_identification(struct hfp_slc_handle *handle,
const char *number,
int type)
{
char cmd[64];
if (handle->is_hsp)
return 0;
if (handle->telephony->call) {
snprintf(cmd, 64, "+CCWA: \"%s\",%d", number, type);
} else {
snprintf(cmd, 64, "+CLIP: \"%s\",%d", number, type);
}
return hfp_send(handle, cmd);
}
/* ATA command to accept an incoming call. Mandatory support per spec 4.13. */
static int answer_call(struct hfp_slc_handle *handle, const char *cmd)
{
int rc;
rc = hfp_send(handle, "OK");
if (rc)
return rc;
return cras_telephony_event_answer_call();
}
/* AT+CCWA command to enable the "Call Waiting notification" function.
* Mandatory support per spec 4.21. */
static int call_waiting_notify(struct hfp_slc_handle *handle, const char *buf)
{
return hfp_send(handle, "OK");
}
/* AT+CLIP command to enable the "Calling Line Identification notification"
* function. Mandatory per spec 4.23.
*/
static int cli_notification(struct hfp_slc_handle *handle, const char *cmd)
{
handle->cli_active = (cmd[8] == '1');
return hfp_send(handle, "OK");
}
/* ATDdd...dd command to place call with supplied number, or ATD>nnn...
* command to dial the number stored at memory location. Mandatory per
* spec 4.18 and 4.19.
*/
static int dial_number(struct hfp_slc_handle *handle, const char *cmd)
{
int rc, cmd_len;
cmd_len = strlen(cmd);
if (cmd[3] == '>') {
/* Handle memory dial. Extract memory location from command
* ATD>nnn...; and lookup. */
int memory_location;
memory_location = strtol(cmd + 4, NULL, 0);
if (handle->telephony->dial_number == NULL || memory_location != 1)
return hfp_send(handle, "ERROR");
}
else {
/* ATDddddd; Store dial number to the only memory slot. */
cras_telephony_store_dial_number(cmd_len - 3 - 1, cmd + 3);
}
rc = hfp_send(handle, "OK");
if (rc)
return rc;
handle->telephony->callsetup = 2;
return hfp_send_ind_event_report(handle, CALLSETUP_IND_INDEX, 2);
}
/* AT+VTS command to generate a DTMF code. Mandatory per spec 4.27. */
static int dtmf_tone(struct hfp_slc_handle *handle, const char *buf)
{
return hfp_send(handle, "OK");
}
/* AT+CMER command enables the registration status update function in AG.
* The service level connection is consider initialized when successfully
* responded OK to the AT+CMER command. Mandatory support per spec 4.4.
*/
static int event_reporting(struct hfp_slc_handle *handle, const char *cmd)
{
char *tokens, *mode, *tmp;
int err = 0;
/* AT+CMER=[<mode>[,<keyp>[,<disp>[,<ind> [,<bfr>]]]]]
* Parse <ind>, the only token we care about.
*/
tokens = strdup(cmd);
strtok(tokens, "=");
mode = strtok(NULL, ",");
tmp = strtok(NULL, ",");
tmp = strtok(NULL, ",");
tmp = strtok(NULL, ",");
/* mode = 3 for forward unsolicited result codes.
* AT+CMER=3,0,0,1 activates “indicator events reporting”.
* The service level connection is considered established after
* successfully responded with OK, regardless of the indicator
* events reporting status.
*/
if (!mode || !tmp) {
syslog(LOG_ERR, "Invalid event reporting” cmd %s", cmd);
err = -EINVAL;
goto event_reporting_err;
}
if (atoi(mode) == FORWARD_UNSOLICIT_RESULT_CODE)
handle->ind_event_report = atoi(tmp);
err = hfp_send(handle, "OK");
if (err) {
syslog(LOG_ERR, "Error sending response for command %s", cmd);
goto event_reporting_err;
}
/* Consider the Service Level Connection to be fully initialized,
* and thereby established, after successfully responded with OK.
*/
if (!handle->initialized) {
handle->initialized = 1;
if (handle->init_cb)
handle->init_cb(handle);
}
event_reporting_err:
free(tokens);
return err;
}
/* AT+CMEE command to set the "Extended Audio Gateway Error Result Code".
* Mandatory per spec 4.9.
*/
static int extended_errors(struct hfp_slc_handle *handle, const char *buf)
{
return hfp_send(handle, "OK");
}
/* AT+CKPD command to handle the user initiated action from headset profile
* device.
*/
static int key_press(struct hfp_slc_handle *handle, const char *buf)
{
hfp_send(handle, "OK");
/* Release the call and connection. */
if (handle->telephony->call || handle->telephony->callsetup) {
cras_telephony_event_terminate_call();
handle->disconnect_cb(handle);
return -EIO;
}
return 0;
}
/* AT+BLDN command to re-dial the last number. Mandatory support
* per spec 4.20.
*/
static int last_dialed_number(struct hfp_slc_handle *handle, const char *buf)
{
int rc;
if (!handle->telephony->dial_number)
return hfp_send(handle, "ERROR");
rc = hfp_send(handle, "OK");
if (rc)
return rc;
handle->telephony->callsetup = 2;
return hfp_send_ind_event_report(handle, CALLSETUP_IND_INDEX, 2);
}
/* AT+CLCC command to query list of current calls. Mandatory support
* per spec 4.31.
*
* +CLCC: <idx>,<direction>,<status>,<mode>,<multiparty>
*/
static int list_current_calls(struct hfp_slc_handle *handle, const char *cmd)
{
char buf[64];
int idx = 1;
int rc;
/* Fake the call list base on callheld and call status
* since we have no API exposed to manage call list.
* This is a hack to pass qualification test which ask us to
* handle the basic case that one call is active and
* the other is on hold. */
if (handle->telephony->callheld)
{
snprintf(buf, 64, "+CLCC: %d,1,1,0,0", idx++);
rc = hfp_send(handle, buf);
if (rc)
return rc;
}
if (handle->telephony->call)
{
snprintf(buf, 64, "+CLCC: %d,1,0,0,0", idx++);
rc = hfp_send(handle, buf);
if (rc)
return rc;
}
return hfp_send(handle, "OK");
}
/* AT+COPS command to query currently selected operator or set name format.
* Mandatory support per spec 4.8.
*/
static int operator_selection(struct hfp_slc_handle *handle, const char *buf)
{
int rc;
if (buf[7] == '?')
{
/* HF sends AT+COPS? command to find current network operator.
* AG responds with +COPS:<mode>,<format>,<operator>, where
* the mode=0 means automatic for network selection. If no
* operator is selected, <format> and <operator> are omitted.
*/
rc = hfp_send(handle, "+COPS: 0");
if (rc)
return rc;
}
return hfp_send(handle, "OK");
}
/* AT+CIND command retrieves the supported indicator and its corresponding
* range and order index or read current status of indicators. Mandatory
* support per spec 4.2.
*/
static int report_indicators(struct hfp_slc_handle *handle, const char *cmd)
{
int err;
char buf[64];
if (cmd[7] == '=') {
/* Indicator update test command "AT+CIND=?" */
err = hfp_send(handle, INDICATOR_UPDATE_RSP);
} else {
/* Indicator update read command "AT+CIND?".
* Respond with current status of AG indicators,
* the values must be listed in the indicator order declared
* in INDICATOR_UPDATE_RSP.
* +CIND: <signal>,<service>,<call>,
* <callsetup>,<callheld>,<roam>
*/
snprintf(buf, 64, "+CIND: %d,%d,%d,%d,%d,%d,0",
handle->battery,
handle->signal,
handle->service,
handle->telephony->call,
handle->telephony->callsetup,
handle->telephony->callheld
);
err = hfp_send(handle, buf);
}
if (err < 0)
return err;
return hfp_send(handle, "OK");
}
/* AT+BIA command to change the subset of indicators that shall be
* sent by the AG. It is okay to ignore this command here since we
* don't do event reporting(CMER).
*/
static int indicator_activation(struct hfp_slc_handle *handle, const char *cmd)
{
/* AT+BIA=[[<indrep 1>][,[<indrep 2>][,...[,[<indrep n>]]]]] */
syslog(LOG_ERR, "Bluetooth indicator activation command %s", cmd);
return hfp_send(handle, "OK");
}
/* AT+VGM and AT+VGS command reports the current mic and speaker gain
* level respectively. Optional support per spec 4.28.
*/
static int signal_gain_setting(struct hfp_slc_handle *handle,
const char *cmd)
{
int gain;
if (strlen(cmd) < 8) {
syslog(LOG_ERR, "Invalid gain setting command %s", cmd);
return -EINVAL;
}
/* Map 0 to the smallest non-zero scale 6/100, and 15 to
* 100/100 full. */
if (cmd[5] == 'S') {
gain = atoi(&cmd[7]);
cras_bt_device_update_hardware_volume(handle->device,
(gain + 1) * 100 / 16);
}
return hfp_send(handle, "OK");
}
/* AT+CNUM command to query the subscriber number. Mandatory support
* per spec 4.30.
*/
static int subscriber_number(struct hfp_slc_handle *handle, const char *buf)
{
return hfp_send(handle, "OK");
}
/* AT+BRSF command notifies the HF(Hands-free device) supported features
* and retrieves the AG(Audio gateway) supported features. Mandatory
* support per spec 4.2.
*/
static int supported_features(struct hfp_slc_handle *handle, const char *cmd)
{
int err;
char response[128];
if (strlen(cmd) < 9)
return -EINVAL;
/* AT+BRSF=<feature> command received, ignore the HF supported feature
* for now. Respond with +BRSF:<feature> to notify mandatory supported
* features in AG(audio gateway).
*/
snprintf(response, 128, "+BRSF: %u", HFP_SUPPORTED_FEATURE);
err = hfp_send(handle, response);
if (err < 0)
return err;
return hfp_send(handle, "OK");
}
int hfp_event_speaker_gain(struct hfp_slc_handle *handle, int gain)
{
char command[128];
/* Normailize gain value to 0-15 */
gain = gain * 15 / 100;
snprintf(command, 128, "+VGS=%d", gain);
return hfp_send(handle, command);
}
/* AT+CHUP command to terminate current call. Mandatory support
* per spec 4.15.
*/
static int terminate_call(struct hfp_slc_handle *handle, const char *cmd)
{
int rc;
rc = hfp_send(handle, "OK");
if (rc)
return rc;
return cras_telephony_event_terminate_call();
}
/* AT commands to support in order to conform HFP specification.
*
* An initialized service level connection is the pre-condition for all
* call related procedures. Note that for the call related commands,
* we are good to just respond with a dummy "OK".
*
* The procedure to establish a service level connection is described below:
*
* 1. HF notifies AG about its own supported features and AG responds
* with its supported feature.
*
* HF(hands-free) AG(audio gateway)
* AT+BRSF=<HF supported feature> -->
* <-- +BRSF:<AG supported feature>
* <-- OK
*
* 2. HF retrieves the information about the indicators supported in AG.
*
* HF(hands-free) AG(audio gateway)
* AT+CIND=? -->
* <-- +CIND:...
* <-- OK
*
* 3. The HF requests the current status of the indicators in AG.
*
* HF(hands-free) AG(audio gateway)
* AT+CIND -->
* <-- +CIND:...
* <-- OK
*
* 4. HF requests enabling indicator status update in the AG.
*
* HF(hands-free) AG(audio gateway)
* AT+CMER= -->
* <-- OK
*/
static struct at_command at_commands[] = {
{ "ATA", answer_call },
{ "ATD", dial_number },
{ "AT+BIA", indicator_activation },
{ "AT+BLDN", last_dialed_number },
{ "AT+BRSF", supported_features },
{ "AT+CCWA", call_waiting_notify },
{ "AT+CHUP", terminate_call },
{ "AT+CIND", report_indicators },
{ "AT+CKPD", key_press },
{ "AT+CLCC", list_current_calls },
{ "AT+CLIP", cli_notification },
{ "AT+CMEE", extended_errors },
{ "AT+CMER", event_reporting },
{ "AT+CNUM", subscriber_number },
{ "AT+COPS", operator_selection },
{ "AT+VG", signal_gain_setting },
{ "AT+VTS", dtmf_tone },
{ 0 }
};
static int handle_at_command(struct hfp_slc_handle *slc_handle,
const char *cmd) {
struct at_command *atc;
for (atc = at_commands; atc->cmd; atc++)
if (!strncmp(cmd, atc->cmd, strlen(atc->cmd)))
return atc->callback(slc_handle, cmd);
syslog(LOG_ERR, "AT command %s not supported", cmd);
return hfp_send(slc_handle, "ERROR");
}
static void slc_watch_callback(void *arg)
{
struct hfp_slc_handle *handle = (struct hfp_slc_handle *)arg;
ssize_t bytes_read;
int err;
bytes_read = read(handle->rfcomm_fd,
&handle->buf[handle->buf_write_idx],
SLC_BUF_SIZE_BYTES - handle->buf_write_idx - 1);
if (bytes_read < 0) {
syslog(LOG_ERR, "Error reading slc command %s",
strerror(errno));
handle->disconnect_cb(handle);
return;
}
handle->buf_write_idx += bytes_read;
handle->buf[handle->buf_write_idx] = '\0';
while (handle->buf_read_idx != handle->buf_write_idx) {
char *end_char;
end_char = strchr(&handle->buf[handle->buf_read_idx], '\r');
if (end_char == NULL)
break;
*end_char = '\0';
err = handle_at_command(handle,
&handle->buf[handle->buf_read_idx]);
if (err < 0)
return;
/* Shift the read index */
handle->buf_read_idx = 1 + end_char - handle->buf;
if (handle->buf_read_idx == handle->buf_write_idx) {
handle->buf_read_idx = 0;
handle->buf_write_idx = 0;
}
}
/* Handle the case when buffer is full and no command found. */
if (handle->buf_write_idx == SLC_BUF_SIZE_BYTES - 1) {
if (handle->buf_read_idx) {
memmove(handle->buf,
&handle->buf[handle->buf_read_idx],
handle->buf_write_idx - handle->buf_read_idx);
handle->buf_write_idx -= handle->buf_read_idx;
handle->buf_read_idx = 0;
} else {
syslog(LOG_ERR,
"Parse SLC command error, clean up buffer");
handle->buf_write_idx = 0;
}
}
return;
}
/* Exported interfaces */
struct hfp_slc_handle *hfp_slc_create(int fd,
int is_hsp,
struct cras_bt_device *device,
hfp_slc_init_cb init_cb,
hfp_slc_disconnect_cb disconnect_cb)
{
struct hfp_slc_handle *handle;
handle = (struct hfp_slc_handle*) calloc(1, sizeof(*handle));
if (!handle)
return NULL;
handle->rfcomm_fd = fd;
handle->is_hsp = is_hsp;
handle->device = device;
handle->init_cb = init_cb;
handle->disconnect_cb = disconnect_cb;
handle->cli_active = 0;
handle->battery = 5;
handle->signal = 5;
handle->service = 1;
handle->ind_event_report = 0;
handle->telephony = cras_telephony_get();
cras_system_add_select_fd(handle->rfcomm_fd,
slc_watch_callback, handle);
return handle;
}
void hfp_slc_destroy(struct hfp_slc_handle *slc_handle)
{
cras_system_rm_select_fd(slc_handle->rfcomm_fd);
close(slc_handle->rfcomm_fd);
free(slc_handle);
}
int hfp_set_call_status(struct hfp_slc_handle *handle, int call)
{
int old_call = handle->telephony->call;
if (old_call == call)
return 0;
handle->telephony->call = call;
return hfp_event_update_call(handle);
}
/* Procedure to setup a call when AG sees incoming call.
*
* HF(hands-free) AG(audio gateway)
* <-- Incoming call
* <-- +CIEV: (callsetup = 1)
* <-- RING (ALERT)
*/
int hfp_event_incoming_call(struct hfp_slc_handle *handle,
const char *number,
int type)
{
int rc;
if (handle->is_hsp)
return 0;
if (handle->cli_active) {
rc = hfp_send_calling_line_identification(handle, number, type);
if (rc)
return rc;
}
if (handle->telephony->call)
return 0;
else
return hfp_send(handle, "RING");
}
int hfp_event_update_call(struct hfp_slc_handle *handle)
{
return hfp_send_ind_event_report(handle, CALL_IND_INDEX,
handle->telephony->call);
}
int hfp_event_update_callsetup(struct hfp_slc_handle *handle)
{
return hfp_send_ind_event_report(handle, CALLSETUP_IND_INDEX,
handle->telephony->callsetup);
}
int hfp_event_update_callheld(struct hfp_slc_handle *handle)
{
return hfp_send_ind_event_report(handle, CALLHELD_IND_INDEX,
handle->telephony->callheld);
}
int hfp_event_set_battery(struct hfp_slc_handle *handle, int level)
{
handle->battery = level;
return hfp_send_ind_event_report(handle, BATTERY_IND_INDEX, level);
}
int hfp_event_set_signal(struct hfp_slc_handle *handle, int level)
{
handle->signal = level;
return hfp_send_ind_event_report(handle, SIGNAL_IND_INDEX, level);
}
int hfp_event_set_service(struct hfp_slc_handle *handle, int avail)
{
/* Convert to 0 or 1.
* Since the value must be either 1 or 0. (service presence or not) */
handle->service = !!avail;
return hfp_send_ind_event_report(handle, SERVICE_IND_INDEX, avail);
}