mirror of
https://github.com/torvalds/linux
synced 2024-09-06 18:01:05 +00:00
![Luiz Augusto von Dentz](/assets/img/avatar_default.png)
Since BT_HS has been remove HCI_AMP controllers no longer has any use so
remove it along with the capability of creating AMP controllers.
Since we no longer need to differentiate between AMP and Primary
controllers, as only HCI_PRIMARY is left, this also remove
hdev->dev_type altogether.
Fixes: e7b02296fb
("Bluetooth: Remove BT_HS")
Signed-off-by: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
703 lines
14 KiB
C
703 lines
14 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
*
|
|
* Bluetooth virtual HCI driver
|
|
*
|
|
* Copyright (C) 2000-2001 Qualcomm Incorporated
|
|
* Copyright (C) 2002-2003 Maxim Krasnyansky <maxk@qualcomm.com>
|
|
* Copyright (C) 2004-2006 Marcel Holtmann <marcel@holtmann.org>
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <asm/unaligned.h>
|
|
|
|
#include <linux/atomic.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/init.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/types.h>
|
|
#include <linux/errno.h>
|
|
#include <linux/sched.h>
|
|
#include <linux/poll.h>
|
|
|
|
#include <linux/skbuff.h>
|
|
#include <linux/miscdevice.h>
|
|
#include <linux/debugfs.h>
|
|
|
|
#include <net/bluetooth/bluetooth.h>
|
|
#include <net/bluetooth/hci_core.h>
|
|
|
|
#define VERSION "1.5"
|
|
|
|
static bool amp;
|
|
|
|
struct vhci_data {
|
|
struct hci_dev *hdev;
|
|
|
|
wait_queue_head_t read_wait;
|
|
struct sk_buff_head readq;
|
|
|
|
struct mutex open_mutex;
|
|
struct delayed_work open_timeout;
|
|
struct work_struct suspend_work;
|
|
|
|
bool suspended;
|
|
bool wakeup;
|
|
__u16 msft_opcode;
|
|
bool aosp_capable;
|
|
atomic_t initialized;
|
|
};
|
|
|
|
static int vhci_open_dev(struct hci_dev *hdev)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static int vhci_close_dev(struct hci_dev *hdev)
|
|
{
|
|
struct vhci_data *data = hci_get_drvdata(hdev);
|
|
|
|
skb_queue_purge(&data->readq);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vhci_flush(struct hci_dev *hdev)
|
|
{
|
|
struct vhci_data *data = hci_get_drvdata(hdev);
|
|
|
|
skb_queue_purge(&data->readq);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vhci_send_frame(struct hci_dev *hdev, struct sk_buff *skb)
|
|
{
|
|
struct vhci_data *data = hci_get_drvdata(hdev);
|
|
|
|
memcpy(skb_push(skb, 1), &hci_skb_pkt_type(skb), 1);
|
|
|
|
skb_queue_tail(&data->readq, skb);
|
|
|
|
if (atomic_read(&data->initialized))
|
|
wake_up_interruptible(&data->read_wait);
|
|
return 0;
|
|
}
|
|
|
|
static int vhci_get_data_path_id(struct hci_dev *hdev, u8 *data_path_id)
|
|
{
|
|
*data_path_id = 0;
|
|
return 0;
|
|
}
|
|
|
|
static int vhci_get_codec_config_data(struct hci_dev *hdev, __u8 type,
|
|
struct bt_codec *codec, __u8 *vnd_len,
|
|
__u8 **vnd_data)
|
|
{
|
|
if (type != ESCO_LINK)
|
|
return -EINVAL;
|
|
|
|
*vnd_len = 0;
|
|
*vnd_data = NULL;
|
|
return 0;
|
|
}
|
|
|
|
static bool vhci_wakeup(struct hci_dev *hdev)
|
|
{
|
|
struct vhci_data *data = hci_get_drvdata(hdev);
|
|
|
|
return data->wakeup;
|
|
}
|
|
|
|
static ssize_t force_suspend_read(struct file *file, char __user *user_buf,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
struct vhci_data *data = file->private_data;
|
|
char buf[3];
|
|
|
|
buf[0] = data->suspended ? 'Y' : 'N';
|
|
buf[1] = '\n';
|
|
buf[2] = '\0';
|
|
return simple_read_from_buffer(user_buf, count, ppos, buf, 2);
|
|
}
|
|
|
|
static void vhci_suspend_work(struct work_struct *work)
|
|
{
|
|
struct vhci_data *data = container_of(work, struct vhci_data,
|
|
suspend_work);
|
|
|
|
if (data->suspended)
|
|
hci_suspend_dev(data->hdev);
|
|
else
|
|
hci_resume_dev(data->hdev);
|
|
}
|
|
|
|
static ssize_t force_suspend_write(struct file *file,
|
|
const char __user *user_buf,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
struct vhci_data *data = file->private_data;
|
|
bool enable;
|
|
int err;
|
|
|
|
err = kstrtobool_from_user(user_buf, count, &enable);
|
|
if (err)
|
|
return err;
|
|
|
|
if (data->suspended == enable)
|
|
return -EALREADY;
|
|
|
|
data->suspended = enable;
|
|
|
|
schedule_work(&data->suspend_work);
|
|
|
|
return count;
|
|
}
|
|
|
|
static const struct file_operations force_suspend_fops = {
|
|
.open = simple_open,
|
|
.read = force_suspend_read,
|
|
.write = force_suspend_write,
|
|
.llseek = default_llseek,
|
|
};
|
|
|
|
static ssize_t force_wakeup_read(struct file *file, char __user *user_buf,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
struct vhci_data *data = file->private_data;
|
|
char buf[3];
|
|
|
|
buf[0] = data->wakeup ? 'Y' : 'N';
|
|
buf[1] = '\n';
|
|
buf[2] = '\0';
|
|
return simple_read_from_buffer(user_buf, count, ppos, buf, 2);
|
|
}
|
|
|
|
static ssize_t force_wakeup_write(struct file *file,
|
|
const char __user *user_buf, size_t count,
|
|
loff_t *ppos)
|
|
{
|
|
struct vhci_data *data = file->private_data;
|
|
bool enable;
|
|
int err;
|
|
|
|
err = kstrtobool_from_user(user_buf, count, &enable);
|
|
if (err)
|
|
return err;
|
|
|
|
if (data->wakeup == enable)
|
|
return -EALREADY;
|
|
|
|
data->wakeup = enable;
|
|
|
|
return count;
|
|
}
|
|
|
|
static const struct file_operations force_wakeup_fops = {
|
|
.open = simple_open,
|
|
.read = force_wakeup_read,
|
|
.write = force_wakeup_write,
|
|
.llseek = default_llseek,
|
|
};
|
|
|
|
static int msft_opcode_set(void *data, u64 val)
|
|
{
|
|
struct vhci_data *vhci = data;
|
|
|
|
if (val > 0xffff || hci_opcode_ogf(val) != 0x3f)
|
|
return -EINVAL;
|
|
|
|
if (vhci->msft_opcode)
|
|
return -EALREADY;
|
|
|
|
vhci->msft_opcode = val;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int msft_opcode_get(void *data, u64 *val)
|
|
{
|
|
struct vhci_data *vhci = data;
|
|
|
|
*val = vhci->msft_opcode;
|
|
|
|
return 0;
|
|
}
|
|
|
|
DEFINE_DEBUGFS_ATTRIBUTE(msft_opcode_fops, msft_opcode_get, msft_opcode_set,
|
|
"%llu\n");
|
|
|
|
static ssize_t aosp_capable_read(struct file *file, char __user *user_buf,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
struct vhci_data *vhci = file->private_data;
|
|
char buf[3];
|
|
|
|
buf[0] = vhci->aosp_capable ? 'Y' : 'N';
|
|
buf[1] = '\n';
|
|
buf[2] = '\0';
|
|
return simple_read_from_buffer(user_buf, count, ppos, buf, 2);
|
|
}
|
|
|
|
static ssize_t aosp_capable_write(struct file *file,
|
|
const char __user *user_buf, size_t count,
|
|
loff_t *ppos)
|
|
{
|
|
struct vhci_data *vhci = file->private_data;
|
|
bool enable;
|
|
int err;
|
|
|
|
err = kstrtobool_from_user(user_buf, count, &enable);
|
|
if (err)
|
|
return err;
|
|
|
|
if (!enable)
|
|
return -EINVAL;
|
|
|
|
if (vhci->aosp_capable)
|
|
return -EALREADY;
|
|
|
|
vhci->aosp_capable = enable;
|
|
|
|
return count;
|
|
}
|
|
|
|
static const struct file_operations aosp_capable_fops = {
|
|
.open = simple_open,
|
|
.read = aosp_capable_read,
|
|
.write = aosp_capable_write,
|
|
.llseek = default_llseek,
|
|
};
|
|
|
|
static int vhci_setup(struct hci_dev *hdev)
|
|
{
|
|
struct vhci_data *vhci = hci_get_drvdata(hdev);
|
|
|
|
if (vhci->msft_opcode)
|
|
hci_set_msft_opcode(hdev, vhci->msft_opcode);
|
|
|
|
if (vhci->aosp_capable)
|
|
hci_set_aosp_capable(hdev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void vhci_coredump(struct hci_dev *hdev)
|
|
{
|
|
/* No need to do anything */
|
|
}
|
|
|
|
static void vhci_coredump_hdr(struct hci_dev *hdev, struct sk_buff *skb)
|
|
{
|
|
char buf[80];
|
|
|
|
snprintf(buf, sizeof(buf), "Controller Name: vhci_ctrl\n");
|
|
skb_put_data(skb, buf, strlen(buf));
|
|
|
|
snprintf(buf, sizeof(buf), "Firmware Version: vhci_fw\n");
|
|
skb_put_data(skb, buf, strlen(buf));
|
|
|
|
snprintf(buf, sizeof(buf), "Driver: vhci_drv\n");
|
|
skb_put_data(skb, buf, strlen(buf));
|
|
|
|
snprintf(buf, sizeof(buf), "Vendor: vhci\n");
|
|
skb_put_data(skb, buf, strlen(buf));
|
|
}
|
|
|
|
#define MAX_COREDUMP_LINE_LEN 40
|
|
|
|
struct devcoredump_test_data {
|
|
enum devcoredump_state state;
|
|
unsigned int timeout;
|
|
char data[MAX_COREDUMP_LINE_LEN];
|
|
};
|
|
|
|
static inline void force_devcd_timeout(struct hci_dev *hdev,
|
|
unsigned int timeout)
|
|
{
|
|
#ifdef CONFIG_DEV_COREDUMP
|
|
hdev->dump.timeout = msecs_to_jiffies(timeout * 1000);
|
|
#endif
|
|
}
|
|
|
|
static ssize_t force_devcd_write(struct file *file, const char __user *user_buf,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
struct vhci_data *data = file->private_data;
|
|
struct hci_dev *hdev = data->hdev;
|
|
struct sk_buff *skb = NULL;
|
|
struct devcoredump_test_data dump_data;
|
|
size_t data_size;
|
|
int ret;
|
|
|
|
if (count < offsetof(struct devcoredump_test_data, data) ||
|
|
count > sizeof(dump_data))
|
|
return -EINVAL;
|
|
|
|
if (copy_from_user(&dump_data, user_buf, count))
|
|
return -EFAULT;
|
|
|
|
data_size = count - offsetof(struct devcoredump_test_data, data);
|
|
skb = alloc_skb(data_size, GFP_ATOMIC);
|
|
if (!skb)
|
|
return -ENOMEM;
|
|
skb_put_data(skb, &dump_data.data, data_size);
|
|
|
|
hci_devcd_register(hdev, vhci_coredump, vhci_coredump_hdr, NULL);
|
|
|
|
/* Force the devcoredump timeout */
|
|
if (dump_data.timeout)
|
|
force_devcd_timeout(hdev, dump_data.timeout);
|
|
|
|
ret = hci_devcd_init(hdev, skb->len);
|
|
if (ret) {
|
|
BT_ERR("Failed to generate devcoredump");
|
|
kfree_skb(skb);
|
|
return ret;
|
|
}
|
|
|
|
hci_devcd_append(hdev, skb);
|
|
|
|
switch (dump_data.state) {
|
|
case HCI_DEVCOREDUMP_DONE:
|
|
hci_devcd_complete(hdev);
|
|
break;
|
|
case HCI_DEVCOREDUMP_ABORT:
|
|
hci_devcd_abort(hdev);
|
|
break;
|
|
case HCI_DEVCOREDUMP_TIMEOUT:
|
|
/* Do nothing */
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static const struct file_operations force_devcoredump_fops = {
|
|
.open = simple_open,
|
|
.write = force_devcd_write,
|
|
};
|
|
|
|
static int __vhci_create_device(struct vhci_data *data, __u8 opcode)
|
|
{
|
|
struct hci_dev *hdev;
|
|
struct sk_buff *skb;
|
|
|
|
if (data->hdev)
|
|
return -EBADFD;
|
|
|
|
/* bits 2-5 are reserved (must be zero) */
|
|
if (opcode & 0x3c)
|
|
return -EINVAL;
|
|
|
|
skb = bt_skb_alloc(4, GFP_KERNEL);
|
|
if (!skb)
|
|
return -ENOMEM;
|
|
|
|
hdev = hci_alloc_dev();
|
|
if (!hdev) {
|
|
kfree_skb(skb);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
data->hdev = hdev;
|
|
|
|
hdev->bus = HCI_VIRTUAL;
|
|
hci_set_drvdata(hdev, data);
|
|
|
|
hdev->open = vhci_open_dev;
|
|
hdev->close = vhci_close_dev;
|
|
hdev->flush = vhci_flush;
|
|
hdev->send = vhci_send_frame;
|
|
hdev->get_data_path_id = vhci_get_data_path_id;
|
|
hdev->get_codec_config_data = vhci_get_codec_config_data;
|
|
hdev->wakeup = vhci_wakeup;
|
|
hdev->setup = vhci_setup;
|
|
set_bit(HCI_QUIRK_NON_PERSISTENT_SETUP, &hdev->quirks);
|
|
|
|
/* bit 6 is for external configuration */
|
|
if (opcode & 0x40)
|
|
set_bit(HCI_QUIRK_EXTERNAL_CONFIG, &hdev->quirks);
|
|
|
|
/* bit 7 is for raw device */
|
|
if (opcode & 0x80)
|
|
set_bit(HCI_QUIRK_RAW_DEVICE, &hdev->quirks);
|
|
|
|
set_bit(HCI_QUIRK_VALID_LE_STATES, &hdev->quirks);
|
|
|
|
if (hci_register_dev(hdev) < 0) {
|
|
BT_ERR("Can't register HCI device");
|
|
hci_free_dev(hdev);
|
|
data->hdev = NULL;
|
|
kfree_skb(skb);
|
|
return -EBUSY;
|
|
}
|
|
|
|
debugfs_create_file("force_suspend", 0644, hdev->debugfs, data,
|
|
&force_suspend_fops);
|
|
|
|
debugfs_create_file("force_wakeup", 0644, hdev->debugfs, data,
|
|
&force_wakeup_fops);
|
|
|
|
if (IS_ENABLED(CONFIG_BT_MSFTEXT))
|
|
debugfs_create_file("msft_opcode", 0644, hdev->debugfs, data,
|
|
&msft_opcode_fops);
|
|
|
|
if (IS_ENABLED(CONFIG_BT_AOSPEXT))
|
|
debugfs_create_file("aosp_capable", 0644, hdev->debugfs, data,
|
|
&aosp_capable_fops);
|
|
|
|
debugfs_create_file("force_devcoredump", 0644, hdev->debugfs, data,
|
|
&force_devcoredump_fops);
|
|
|
|
hci_skb_pkt_type(skb) = HCI_VENDOR_PKT;
|
|
|
|
skb_put_u8(skb, 0xff);
|
|
skb_put_u8(skb, opcode);
|
|
put_unaligned_le16(hdev->id, skb_put(skb, 2));
|
|
skb_queue_head(&data->readq, skb);
|
|
atomic_inc(&data->initialized);
|
|
|
|
wake_up_interruptible(&data->read_wait);
|
|
return 0;
|
|
}
|
|
|
|
static int vhci_create_device(struct vhci_data *data, __u8 opcode)
|
|
{
|
|
int err;
|
|
|
|
mutex_lock(&data->open_mutex);
|
|
err = __vhci_create_device(data, opcode);
|
|
mutex_unlock(&data->open_mutex);
|
|
|
|
return err;
|
|
}
|
|
|
|
static inline ssize_t vhci_get_user(struct vhci_data *data,
|
|
struct iov_iter *from)
|
|
{
|
|
size_t len = iov_iter_count(from);
|
|
struct sk_buff *skb;
|
|
__u8 pkt_type, opcode;
|
|
int ret;
|
|
|
|
if (len < 2 || len > HCI_MAX_FRAME_SIZE)
|
|
return -EINVAL;
|
|
|
|
skb = bt_skb_alloc(len, GFP_KERNEL);
|
|
if (!skb)
|
|
return -ENOMEM;
|
|
|
|
if (!copy_from_iter_full(skb_put(skb, len), len, from)) {
|
|
kfree_skb(skb);
|
|
return -EFAULT;
|
|
}
|
|
|
|
pkt_type = *((__u8 *) skb->data);
|
|
skb_pull(skb, 1);
|
|
|
|
switch (pkt_type) {
|
|
case HCI_EVENT_PKT:
|
|
case HCI_ACLDATA_PKT:
|
|
case HCI_SCODATA_PKT:
|
|
case HCI_ISODATA_PKT:
|
|
if (!data->hdev) {
|
|
kfree_skb(skb);
|
|
return -ENODEV;
|
|
}
|
|
|
|
hci_skb_pkt_type(skb) = pkt_type;
|
|
|
|
ret = hci_recv_frame(data->hdev, skb);
|
|
break;
|
|
|
|
case HCI_VENDOR_PKT:
|
|
cancel_delayed_work_sync(&data->open_timeout);
|
|
|
|
opcode = *((__u8 *) skb->data);
|
|
skb_pull(skb, 1);
|
|
|
|
if (skb->len > 0) {
|
|
kfree_skb(skb);
|
|
return -EINVAL;
|
|
}
|
|
|
|
kfree_skb(skb);
|
|
|
|
ret = vhci_create_device(data, opcode);
|
|
break;
|
|
|
|
default:
|
|
kfree_skb(skb);
|
|
return -EINVAL;
|
|
}
|
|
|
|
return (ret < 0) ? ret : len;
|
|
}
|
|
|
|
static inline ssize_t vhci_put_user(struct vhci_data *data,
|
|
struct sk_buff *skb,
|
|
char __user *buf, int count)
|
|
{
|
|
char __user *ptr = buf;
|
|
int len;
|
|
|
|
len = min_t(unsigned int, skb->len, count);
|
|
|
|
if (copy_to_user(ptr, skb->data, len))
|
|
return -EFAULT;
|
|
|
|
if (!data->hdev)
|
|
return len;
|
|
|
|
data->hdev->stat.byte_tx += len;
|
|
|
|
switch (hci_skb_pkt_type(skb)) {
|
|
case HCI_COMMAND_PKT:
|
|
data->hdev->stat.cmd_tx++;
|
|
break;
|
|
case HCI_ACLDATA_PKT:
|
|
data->hdev->stat.acl_tx++;
|
|
break;
|
|
case HCI_SCODATA_PKT:
|
|
data->hdev->stat.sco_tx++;
|
|
break;
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
static ssize_t vhci_read(struct file *file,
|
|
char __user *buf, size_t count, loff_t *pos)
|
|
{
|
|
struct vhci_data *data = file->private_data;
|
|
struct sk_buff *skb;
|
|
ssize_t ret = 0;
|
|
|
|
while (count) {
|
|
skb = skb_dequeue(&data->readq);
|
|
if (skb) {
|
|
ret = vhci_put_user(data, skb, buf, count);
|
|
if (ret < 0)
|
|
skb_queue_head(&data->readq, skb);
|
|
else
|
|
kfree_skb(skb);
|
|
break;
|
|
}
|
|
|
|
if (file->f_flags & O_NONBLOCK) {
|
|
ret = -EAGAIN;
|
|
break;
|
|
}
|
|
|
|
ret = wait_event_interruptible(data->read_wait,
|
|
!skb_queue_empty(&data->readq));
|
|
if (ret < 0)
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static ssize_t vhci_write(struct kiocb *iocb, struct iov_iter *from)
|
|
{
|
|
struct file *file = iocb->ki_filp;
|
|
struct vhci_data *data = file->private_data;
|
|
|
|
return vhci_get_user(data, from);
|
|
}
|
|
|
|
static __poll_t vhci_poll(struct file *file, poll_table *wait)
|
|
{
|
|
struct vhci_data *data = file->private_data;
|
|
|
|
poll_wait(file, &data->read_wait, wait);
|
|
|
|
if (!skb_queue_empty(&data->readq))
|
|
return EPOLLIN | EPOLLRDNORM;
|
|
|
|
return EPOLLOUT | EPOLLWRNORM;
|
|
}
|
|
|
|
static void vhci_open_timeout(struct work_struct *work)
|
|
{
|
|
struct vhci_data *data = container_of(work, struct vhci_data,
|
|
open_timeout.work);
|
|
|
|
vhci_create_device(data, 0x00);
|
|
}
|
|
|
|
static int vhci_open(struct inode *inode, struct file *file)
|
|
{
|
|
struct vhci_data *data;
|
|
|
|
data = kzalloc(sizeof(struct vhci_data), GFP_KERNEL);
|
|
if (!data)
|
|
return -ENOMEM;
|
|
|
|
skb_queue_head_init(&data->readq);
|
|
init_waitqueue_head(&data->read_wait);
|
|
|
|
mutex_init(&data->open_mutex);
|
|
INIT_DELAYED_WORK(&data->open_timeout, vhci_open_timeout);
|
|
INIT_WORK(&data->suspend_work, vhci_suspend_work);
|
|
|
|
file->private_data = data;
|
|
nonseekable_open(inode, file);
|
|
|
|
schedule_delayed_work(&data->open_timeout, msecs_to_jiffies(1000));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vhci_release(struct inode *inode, struct file *file)
|
|
{
|
|
struct vhci_data *data = file->private_data;
|
|
struct hci_dev *hdev;
|
|
|
|
cancel_delayed_work_sync(&data->open_timeout);
|
|
flush_work(&data->suspend_work);
|
|
|
|
hdev = data->hdev;
|
|
|
|
if (hdev) {
|
|
hci_unregister_dev(hdev);
|
|
hci_free_dev(hdev);
|
|
}
|
|
|
|
skb_queue_purge(&data->readq);
|
|
file->private_data = NULL;
|
|
kfree(data);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct file_operations vhci_fops = {
|
|
.owner = THIS_MODULE,
|
|
.read = vhci_read,
|
|
.write_iter = vhci_write,
|
|
.poll = vhci_poll,
|
|
.open = vhci_open,
|
|
.release = vhci_release,
|
|
.llseek = no_llseek,
|
|
};
|
|
|
|
static struct miscdevice vhci_miscdev = {
|
|
.name = "vhci",
|
|
.fops = &vhci_fops,
|
|
.minor = VHCI_MINOR,
|
|
};
|
|
module_misc_device(vhci_miscdev);
|
|
|
|
module_param(amp, bool, 0644);
|
|
MODULE_PARM_DESC(amp, "Create AMP controller device");
|
|
|
|
MODULE_AUTHOR("Marcel Holtmann <marcel@holtmann.org>");
|
|
MODULE_DESCRIPTION("Bluetooth virtual HCI driver ver " VERSION);
|
|
MODULE_VERSION(VERSION);
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_ALIAS("devname:vhci");
|
|
MODULE_ALIAS_MISCDEV(VHCI_MINOR);
|