blob: 8c78db9944e0f50443d2f9d24e052e3e67240603 [file] [log] [blame]
/*
* 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.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <getopt.h>
#include <glob.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <alsa/asoundlib.h>
#include <linux/input.h>
#include <linux/uinput.h>
#define DAEMON_NAME "jabra_vold"
/* endpoint used for the HID interrupt */
#define JABRA_ENDPOINT 0x81
/*
* Report numbers used by the Jabra device.
*
* We could parse them from the HID descriptors,
* but that's long and complicated.
*/
enum jabra_report {
JABRA_HID_REPORT = 1,
JABRA_TELEPHONY_REPORT = 3,
};
/* bit for mute key press in the telephony report */
#define MUTE_FLAG 0x08
/* Names used by the kernel Alsa audio driver */
#define ALSA_DEV_NAME "Jabra SPEAK"
#define ALSA_CONTROL_NAME "PCM Playback Volume"
#define KEY_CROS_VOLDOWN KEY_VOLUMEDOWN
#define KEY_CROS_VOLUP KEY_VOLUMEUP
/* Maximum number of supported Alsa devices for Jabra */
#define ALSA_DEV_COUNT_MAX 4
/*
* Linux kernel usbmon binary interface format
* from Documentation/usb/usbmon.txt
*
* Only the 48 first bytes as we are using read() rather the IOCTL
* which returns the full header.
*/
struct usbmon_packet {
uint64_t id; /* 0: URB ID - from submission to callback */
unsigned char type; /* 8: Same as text; extensible. */
unsigned char xfer_type; /* ISO (0), Intr, Control, Bulk (3) */
unsigned char epnum; /* Endpoint number and transfer direction */
unsigned char devnum; /* Device address */
uint16_t busnum; /* 12: Bus number */
char flag_setup; /* 14: Same as text */
char flag_data; /* 15: Same as text; Binary zero is OK. */
int64_t ts_sec; /* 16: gettimeofday */
int32_t ts_usec; /* 24: gettimeofday */
int status; /* 28: */
unsigned int length; /* 32: Length of data (submitted or actual) */
unsigned int len_cap; /* 36: Delivered length */
union { /* 40: */
unsigned char setup[8]; /* Only for Control S-type */
struct iso_rec { /* Only for ISO */
int error_count;
int numdesc;
} iso;
} s;
};
/* Device node for the usbmon binary interface */
#define USBMON_DEV_FMT "/dev/usbmon%d"
/* device context */
struct jabra_dev {
int uinput_fd;
int alsa_dev_count;
snd_hctl_t *hctl[ALSA_DEV_COUNT_MAX];
snd_hctl_elem_t *playback_vol[ALSA_DEV_COUNT_MAX];
int mon_fd;
int devnum;
};
static void logd(int level, const char *fmt, ...)
__attribute__((format (printf, 2, 3)));
static void logd(int level, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vsyslog(level, fmt, ap);
va_end(ap);
}
static int alsa_touch_volume(struct jabra_dev *dev)
{
snd_ctl_elem_value_t *elem;
int err = 0, idx;
snd_ctl_elem_value_alloca(&elem);
for (idx = 0; idx < dev->alsa_dev_count; ++idx) {
err = snd_hctl_elem_read(dev->playback_vol[idx], elem);
if (err < 0)
goto return_err;
err = snd_hctl_elem_write(dev->playback_vol[idx], elem);
if (err < 0)
goto return_err;
logd(LOG_INFO, "Touch volume: %ld\n",
snd_ctl_elem_value_get_integer(elem, 0));
}
return_err:
return (err < 0) ? err : 0;
}
static int alsa_init(struct jabra_dev *dev)
{
int ret;
snd_ctl_elem_id_t *id;
int idx;
char *card_name;
char *hwid[ALSA_DEV_COUNT_MAX] = { NULL, };
time_t deadline = time(NULL) + 5 /* 5-second timeout */;
/* allow a brief initial delay so that multiple ALSA devices can be found */
usleep(500000);
while (time(NULL) < deadline) {
/* find the Jabra device id */
for (idx = -1; (snd_card_next(&idx) == 0) && (idx >= 0); ) {
ret = snd_card_get_name(idx, &card_name);
if (!ret && (strncmp(card_name, ALSA_DEV_NAME,
sizeof(ALSA_DEV_NAME) - 1) == 0)) {
ret = asprintf(&hwid[dev->alsa_dev_count], "hw:%d", idx);
logd(LOG_NOTICE, "ALSA dev %s (%s)\n",
hwid[dev->alsa_dev_count], card_name);
free(card_name);
if (dev->alsa_dev_count++ >= ALSA_DEV_COUNT_MAX) {
logd(LOG_ERR, "Too many ALSA devices found for Jabra\n");
break;
}
}
if (!ret)
free(card_name);
}
if (dev->alsa_dev_count)
break;
logd(LOG_INFO, "no ALSA device, retry later ...\n");
/*
* card not found, loading the kernel module and initializing
* the driver might take some time, retry later.
*/
usleep(250000);
}
if (!dev->alsa_dev_count) {
logd(LOG_ERR, "Cannot find ALSA device for Jabra\n");
return -ENODEV;
}
/* initialize the Jabra USB-audio mixer control */
for (idx = 0; idx < dev->alsa_dev_count; ++idx) {
ret = snd_hctl_open(&dev->hctl[idx], hwid[idx], 0);
if (ret < 0)
return -ENODEV;
ret = snd_hctl_load(dev->hctl[idx]);
if (ret < 0)
goto fail_after_open;
snd_ctl_elem_id_alloca(&id);
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
snd_ctl_elem_id_set_name(id, ALSA_CONTROL_NAME);
dev->playback_vol[idx] = snd_hctl_find_elem(dev->hctl[idx], id);
if (!dev->playback_vol[idx])
goto fail_after_open;
free(hwid[idx]);
}
return 0;
fail_after_open:
logd(LOG_ERR, "alsa initialization failed (%d)\n", ret);
for (idx = 0; idx < dev->alsa_dev_count; ++idx) {
if (dev->hctl[idx]) {
snd_hctl_close(dev->hctl[idx]);
dev->hctl[idx] = NULL;
}
free(hwid[idx]);
}
return ret;
}
static int hid_init(struct jabra_dev *dev)
{
int ret;
size_t count;
char* uidev_ptr;
struct uinput_user_dev *uidev = calloc(1, sizeof(struct uinput_user_dev));
time_t deadline = time(NULL) + 5 /* 5-second timeout */;
if (!uidev) {
logd(LOG_ERR, "Failed to allocate uinput_user_dev buffer");
return -ENOMEM;
}
while (time(NULL) < deadline) {
dev->uinput_fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
if (dev->uinput_fd >= 0)
break;
logd(LOG_INFO, "uinput not started, retry later ...\n");
/*
* uinput not started, retry later.
*/
usleep(250000);
}
if (dev->uinput_fd < 0) {
logd(LOG_ERR, "Cannot open uinput for HID event injection\n");
return -ENOMEM;
}
ioctl(dev->uinput_fd, UI_SET_EVBIT, EV_KEY);
ioctl(dev->uinput_fd, UI_SET_EVBIT, EV_SYN);
ioctl(dev->uinput_fd, UI_SET_KEYBIT, KEY_CROS_VOLDOWN);
ioctl(dev->uinput_fd, UI_SET_KEYBIT, KEY_CROS_VOLUP);
snprintf(uidev->name, UINPUT_MAX_NAME_SIZE, "jabra_vold input device");
uidev->id.bustype = BUS_USB;
uidev->id.version = 1;
for (uidev_ptr = (char*)uidev, count = sizeof(*uidev); count; ) {
int written = write(dev->uinput_fd, uidev_ptr, count);
if (written < 0) {
if (errno == EINTR || errno == EAGAIN) continue;
logd(LOG_ERR, "Failed to configure input device\n");
ret = errno;
goto fail_exit;
} else {
uidev_ptr += written;
count -= written;
}
}
ret = ioctl(dev->uinput_fd, UI_DEV_CREATE);
if (ret < 0) {
logd(LOG_ERR, "Failed to create input device\n");
goto fail_exit;
}
free(uidev);
return 0;
fail_exit:
close(dev->uinput_fd);
dev->uinput_fd = 0;
free(uidev);
return ret;
}
static void inject_hid_event(struct jabra_dev *dev, uint8_t key)
{
struct input_event evt = { .type = EV_KEY, .code = key, .value = 1 };
struct input_event syn = { .type = EV_SYN, .code = SYN_REPORT };
int ret;
ret = gettimeofday(&evt.time, NULL);
if (ret)
logd(LOG_WARNING, "Cannot get time\n");
/* key press event */
ret = write(dev->uinput_fd, &evt, sizeof(evt));
if (ret < 0)
logd(LOG_WARNING, "Cannot inject HID event\n");
/* key release event */
evt.value = 0;
ret = write(dev->uinput_fd, &evt, sizeof(evt));
if (ret < 0)
logd(LOG_WARNING, "Cannot inject HID event\n");
syn.time = evt.time;
ret = write(dev->uinput_fd, &syn, sizeof(syn));
if (ret < 0)
logd(LOG_WARNING, "Cannot inject HID SYN report\n");
}
static void handle_hid_event(struct jabra_dev *dev,
uint8_t report, uint8_t code)
{
if (report == JABRA_HID_REPORT) { /* volume keys */
switch(code)
{
case 0: /* volume key release */
/* touch the volume on the USB Audio interface
* to ensure we receive the next one
*/
alsa_touch_volume(dev);
break;
case 1: /* volume- press */
inject_hid_event(dev, KEY_CROS_VOLDOWN);
break;
case 2: /* volume+ press */
inject_hid_event(dev, KEY_CROS_VOLUP);
break;
}
} else if (report == JABRA_TELEPHONY_REPORT) { /* other keys */
/*
* if 'code' has the MUTE_FLAG bit set, the user has pressed
* the mic mute button, but the UI is not handling mic mute key
* so far (only speaker mute), so we don't inject any HID event.
*/
} else {
logd(LOG_NOTICE, "HID message %02x %02x\n", report, code);
}
}
static int usbmon_poll(struct jabra_dev *dev)
{
struct pkt {
struct usbmon_packet hdr;
uint8_t payload[64];
} pkt;
ssize_t rlen;
while (1) {
rlen = read(dev->mon_fd, &pkt, sizeof(pkt));
if (rlen <= 0)
return rlen;
if ((pkt.hdr.devnum == dev->devnum) &&
(pkt.hdr.epnum == JABRA_ENDPOINT) &&
(pkt.hdr.type == 'C')) {
if ((pkt.hdr.status == 0) && (pkt.hdr.len_cap == 3))
handle_hid_event(dev, pkt.payload[0],
pkt.payload[1]);
if (pkt.hdr.status == -EPROTO) {
logd(LOG_NOTICE,
"Comm failed : disconnected ?\n");
return 0;
}
}
}
}
static int usbmon_open(struct jabra_dev *dev, int busnum)
{
char *node;
int ret;
ret = asprintf(&node, USBMON_DEV_FMT, busnum);
if (ret < 0)
return ret;
dev->mon_fd = open(node, O_RDONLY);
if (dev->mon_fd < 0) {
dev->mon_fd = 0;
ret = errno;
logd(LOG_ERR, "Cannot open usbmon node %s (%d)\n",
node, ret);
goto fail_free;
}
logd(LOG_INFO,"Using %s bus %d device %d\n", node, busnum, dev->devnum);
ret = 0;
fail_free:
free(node);
return ret;
}
static int use_jabra_device(int busnum, int devnum, int loglvl, int errlog)
{
struct jabra_dev *dev;
int idx, ret;
/* setup the syslog facility and honor UDEV log level if set */
openlog(DAEMON_NAME, errlog ? LOG_PERROR : 0, LOG_DAEMON);
setlogmask(LOG_UPTO(loglvl));
dev = calloc(1, sizeof(struct jabra_dev));
if (!dev)
return -ENOMEM;
dev->devnum = devnum;
ret = hid_init(dev);
if (ret)
goto err;
ret = alsa_init(dev);
if (ret)
goto err;
ret = usbmon_open(dev, busnum);
if (ret)
goto err;
usbmon_poll(dev);
err:
if (dev->mon_fd)
close(dev->mon_fd);
for (idx = 0; idx < dev->alsa_dev_count; ++idx) {
if (dev->hctl[idx])
snd_hctl_close(dev->hctl[idx]);
}
if (dev->uinput_fd) {
ioctl(dev->uinput_fd, UI_DEV_DESTROY);
close(dev->uinput_fd);
}
free(dev);
closelog();
return ret;
}
int main(int argc, char *argv[])
{
int opt;
char *action_str = "start";
char *busnum_str = getenv("BUSNUM");
char *devnum_str = getenv("DEVNUM");
char *loglevel_str = getenv("LOGLEVEL");
char pid_filename[128];
int use_stderr = 0;
int busnum, devnum;
int loglevel = LOG_NOTICE;
int pid;
while ((opt = getopt(argc, argv, "a:b:d:n:")) != -1) {
switch (opt) {
case 'a':
action_str = optarg;
break;
case 'b':
busnum_str = optarg;
break;
case 'd':
loglevel_str = optarg;
use_stderr = 1;
break;
case 'n':
devnum_str = optarg;
break;
}
}
if (!busnum_str || !devnum_str) {
fprintf(stderr, "USB bus/device not defined\n");
return -ENODEV;
}
busnum = atoi(busnum_str);
devnum = atoi(devnum_str);
if (loglevel_str)
loglevel = atoi(loglevel_str);
snprintf(pid_filename, sizeof(pid_filename), "/var/run/jabra_vold/jabra_vold.%d.%d.pid",
busnum, devnum);
if (strcmp(action_str, "start") == 0) {
pid = fork();
if (pid < 0) {
fprintf(stderr, "Failed to fork child process\n");
return -ENOMEM;
}
if (pid != 0) {
logd(LOG_INFO, "Started jabra_vold child process %d.\n", pid);
return 0;
}
pid = fork();
if (pid < 0) {
fprintf(stderr, "Failed to fork grandchild process\n");
return -ENOMEM;
}
if (pid != 0) {
FILE* pid_file = fopen(pid_filename, "r");
if (pid_file) {
int pid_to_kill;
if (fscanf(pid_file, "%d", &pid_to_kill) > 0) {
logd(LOG_INFO, "Stopping stale jabra_vold process %d\n", pid_to_kill);
kill(pid_to_kill, SIGKILL);
}
fclose(pid_file);
}
pid_file = fopen(pid_filename, "w");
if (!pid_file) {
fprintf(stderr, "Failed to open PID file to write for jabra_vold %d:%d\n",
busnum, devnum);
return -ENOMEM;
}
logd(LOG_INFO, "Started jabra_vold grandchild process %d.\n", pid);
if (fprintf(pid_file, "%d\n", pid) <= 0) {
fprintf(stderr, "Failed to write PID to the PID file.\n");
return -ENOMEM;
}
fclose(pid_file);
return 0;
}
} else if (strcmp(action_str, "stop") == 0) {
FILE* pid_file = fopen(pid_filename, "r");
if (!pid_file) {
fprintf(stderr, "Failed to open PID file to read for jabra_vold %d:%d\n",
busnum, devnum);
return -ENOMEM;
}
if (fscanf(pid_file, "%d", &pid) <= 0) {
fprintf(stderr, "Failed to obtain PID for jabra_vold %d:%d\n",
busnum, devnum);
return -ENOMEM;
}
fclose(pid_file);
remove(pid_filename);
logd(LOG_INFO, "Stopping jabra_vold process %d\n", pid);
kill(pid, SIGKILL);
return 0;
} else {
fprintf(stderr, "Unrecognized action string '%s'\n", action_str);
return -EINVAL;
}
return use_jabra_device(busnum, devnum, loglevel, use_stderr);
}