本文分析蓝牙协议栈中蓝牙转串口(SPP)部分的实现。
Bluez提供了蓝牙转串口的功能,应用程序可以通过dbus接口控制bluez的串口功能。
org.bluez.SerialProxyManager->CreateProxy // 得到一个serial proxy
org.bluez.SerialProxy->SetSerialParameters // 设置串口参数
org.bluez.SerialProxy->Enable // 启动串口
之后,bluez就会等待远端设备连接本地的串口服务
当远端设备与本地设备之间建立连接,并且本地设备发现远端设备包含SPP功能,则Bluez将会注册一个“org.bluez.Serial”接口的实例。应用程序可通过调用其中的”Connect”接口,建立与远端设备之间的串口连接。
与SPP有关的代码在bluez的serial目录中,其初始化函数为serial_init。
static int serial_init(void)
。。。
serial_manager_init(connection);
int serial_manager_init(DBusConnection *conn)
。。。
btd_register_adapter_driver(&serial_proxy_driver);
btd_register_device_driver(&serial_port_driver);
serial_manager_init函数中包含两个操作,一个是注册serial_proxy_driver,另一个是注册serial_port_driver。
static struct btd_adapter_driver serial_proxy_driver = {
.name = "serial-proxy",
.probe = proxy_probe,
.remove = proxy_remove,
};
Bluez启动后,对于每一个蓝牙适配器设备,都会调用其中的proxy_probe函数。
static int proxy_probe(struct btd_adapter *adapter)
。。。
proxy_register(connection, adapter);
int proxy_register(DBusConnection *conn, struct btd_adapter *btd_adapter)
。。。
struct serial_adapter *adapter = g_new0(struct serial_adapter, 1);
adapter->conn = dbus_connection_ref(conn);
adapter->btd_adapter = btd_adapter_ref(btd_adapter);
path = adapter_get_path(btd_adapter);
// 注册SerialProxyManager接口
g_dbus_register_interface(conn, path, SERIAL_MANAGER_INTERFACE,
manager_methods, manager_signals, NULL,
adapter, manager_path_unregister);
adapters = g_slist_append(adapters, adapter);
// 这里是根据配置文件注册默认的SerialProxyManager接口,可以跳过
serial_proxy_init(adapter);
SerialProxyManager接口的方法如下:
static GDBusMethodTable manager_methods[] = {
{ "CreateProxy", "ss", "s", create_proxy },
{ "ListProxies", "", "as", list_proxies },
{ "RemoveProxy", "s", "", remove_proxy },
{ },
};
当应用程序需要建立一个serial proxy实例时,可以调用其中的CreateProxy方法,此方法会映射到create_proxy函数。
static DBusMessage *create_proxy(DBusConnection *conn,
DBusMessage *msg, void *data)
// 得到应用程序传下来的参数
dbus_message_get_args(msg, NULL,
DBUS_TYPE_STRING, &pattern,
DBUS_TYPE_STRING, &address,
DBUS_TYPE_INVALID);
// 得到uuid
uuid_str = bt_name2string(pattern);
register_proxy(adapter, uuid_str, address, &proxy);
。。。
static int register_proxy(struct serial_adapter *adapter,
const char *uuid_str, const char *address,
struct serial_proxy **proxy)
// 根据应用程序传下来的地址名称,判断地址类型。类型可以是unix socket、串口或者tcp socket。此socket用于bluez与应用程序之间的串口数据通信。如果应用程序发送数据,就写入此socket;如果应用程序接收数据,就由Bluez写入此socket。address到类型的转换细节可以参考函数的具体实现。
type = addr2type(address);
。。。
// 以下是根据地址类型进入相应的初始化函数
switch (type) {
case UNIX_SOCKET_PROXY:
err = proxy_socket_register(adapter, uuid_str, address, proxy);
break;
case TTY_PROXY:
err = proxy_tty_register(adapter, uuid_str, address, NULL,
proxy);
break;
case TCP_SOCKET_PROXY:
err = proxy_tcp_register(adapter, uuid_str, address, proxy);
break;
default:
err = -EINVAL;
}
。。。
由于三种地质类型的初始化大同小异,所以这里只研究unix socket类型的情况:
当上层应用使用unix socket时,由应用程序建立一个此类型的socket,然后把socket地址传给Bluez,Bluez同样会根据此地址创建socket。之后,Bluez和应用程序之间就可以通过此socket像操作管道一样互相传递数据。在Bluez的实现中,此socket被用于传递蓝牙的串口数据。
static int proxy_socket_register(struct serial_adapter *adapter,
const char *uuid128, const char *address,
struct serial_proxy **proxy)
struct serial_proxy *prx;
prx->address = g_strdup(address);
prx->uuid128 = g_strdup(uuid128);
prx->type = UNIX_SOCKET_PROXY;
adapter_get_address(adapter->btd_adapter, &prx->src);
prx->adapter = adapter;
register_proxy_object(prx);
static int register_proxy_object(struct serial_proxy *prx)
。。。
g_dbus_register_interface(adapter->conn, path,
SERIAL_PROXY_INTERFACE,
proxy_methods, NULL, NULL,
prx, proxy_path_unregister);
。。。
以上就是create_proxy的全部操作。到这里仅仅建立了一个串口proxy,但此proxy还未使能。使能操作由应用程序调用上面新注册的接口SERIAL_PROXY_INTERFACE中的Enable方法完成。
static GDBusMethodTable proxy_methods[] = {
{ "Enable", "", "", proxy_enable },
{ "Disable", "", "", proxy_disable },
{ "GetInfo", "", "a{sv}",proxy_get_info },
{ "SetSerialParameters", "syys", "", proxy_set_serial_params },
{ },
};
static DBusMessage *proxy_enable(DBusConnection *conn,
DBusMessage *msg, void *data)
。。。
enable_proxy(prx);
。。。
static int enable_proxy(struct serial_proxy *prx)
。。。
// 监听RFCOMM通道
prx->io = bt_io_listen(BT_IO_RFCOMM, NULL, confirm_event_cb, prx,
NULL, &gerr,
BT_IO_OPT_SOURCE_BDADDR, &prx->src,
BT_IO_OPT_INVALID);
// 当调用listen时,Bluez(本地蓝牙适配器)会分配一个channel,然后在这个channel上等待对方连接。这个channel很重要,因为蓝牙协议中根据channel号区分RFCOMM上的各种应用
bt_io_get(prx->io, BT_IO_RFCOMM, &gerr,
BT_IO_OPT_CHANNEL, &prx->channel,
BT_IO_OPT_INVALID);
。。。
sdp_record_t *record;
// 分配一个SDP record,这里把channel号设置到了SDP record里面
record = proxy_record_new(prx->uuid128, prx->channel);
// 将record加入到SDP service中,到这里远端设备就能够看到本地设备的蓝牙服务了。该服务在RFCOMM的prx->channel中
add_record_to_server(&prx->src, record);
prx->record_id = record->handle;
。。。
到这里,本地设备已经准备好接受远端设备的串口连接,对方连接时,上面调用的bt_io_listen中的confirm_event_cb回调函数将被调用。
在confirm_event_cb中,为执行鉴权操作,鉴权成功后调用bt_io_accept建立连接。Accept成功后,回调函数connect_event_cb将被调用。
static void connect_event_cb(GIOChannel *chan, GError *conn_err, gpointer data)
。。。
switch (prx->type)
case UNIX_SOCKET_PROXY:
sk = unix_socket_connect(prx->address);
break;
case TTY_PROXY:
。。。
case TCP_SOCKET_PROXY:
。。。
g_io_add_watch(prx->rfcomm,
G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
forward_data, prx);
g_io_add_watch(prx->local,
G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
forward_data, prx);
。。。
unix_socket_connect函数中将根据address建立一个unix socket,并执行了connect。注意到上层应用同样建立了这么一个socket,如果此时上层应用调用select等待在此socket中,那么这里调用connect后,上层应用将得到通知,从而得知与远端设备的连接已经建立。
接下来再看看g_io_add_watch的调用。这里调用两次,一次是针对远端设备的socket;一次是针对与上层应用之间的socket,即刚建立的unix socket。这两个socket之间有一个配对关系,一个用于与远端设备的串口数据传输,另一个用于与上层应用的数据传输。这两个通道关联后就建立了远端设备与上层应用之间的连接,这个关联就由两次g_io_add_watch调用的回调函数forward_data实现。
forward_data被调用时,说明相应的通道上有数据,其伪代码如下:
1. 得到通道上的数据
2. 将数据发送到另一个通道。
通过此种方式,两个通道之间实现了关联。
static struct btd_device_driver serial_port_driver = {
.name = "serial-port",
.uuids = BTD_UUIDS(RFCOMM_UUID_STR),
.probe = port_probe,
.remove = port_remove,
};
当包含UUID RFCOMM_UUID_STR的远端设备连接到本地时,Bluez就会调用serial_port_driver的port_probe函数。
static int port_probe(struct btd_device *device, GSList *uuids)
while (uuids) {
serial_probe(device, uuids->data);
uuids = uuids->next;
}
static int serial_probe(struct btd_device *device, const char *uuid)
const sdp_record_t * rec = btd_device_get_record(device, uuid);
// 从SDP record中得到channel号,此channel用于串口通信
int ch = sdp_get_proto_port(protos, RFCOMM_UUID);
。。。
port_register(connection, path, &src, &dst, uuid, ch);
int port_register(DBusConnection *conn, const char *path, bdaddr_t *src,
bdaddr_t *dst, const char *uuid, uint8_t channel)
。。。
create_serial_device(conn, path, src, dst);
。。。
static struct serial_device *create_serial_device(DBusConnection *conn,
const char *path, bdaddr_t *src,
bdaddr_t *dst)
// 新建一个serial_device
Struct serial_device* device = g_new0(struct serial_device, 1);
。。。
// 注册一个SERIAL_PORT_INTERFACE接口
g_dbus_register_interface(conn, path, SERIAL_PORT_INTERFACE,
port_methods, NULL, NULL, device, path_unregister);
以上为本地设备发现一个支持SPP的远端设备之后执行的操作,最后就是对这个设备新建了一个SERIAL_PORT_INTERFACE接口的实例。此接口的定义为:
static GDBusMethodTable port_methods[] = {
{ "Connect", "s", "s", port_connect, G_DBUS_METHOD_FLAG_ASYNC },
{ "Disconnect", "s", "", port_disconnect },
{ }
};
当上层应用需要主动连接远端设备,而不是被动等待远端设备连接本地设备时,就可以调用这里的connect方法,此方法最终映射到port_connect函数。
Port_connect函数最终是调用到bt_io_connect函数。
bt_io_connect (BT_IO_RFCOMM, rfcomm_connect_cb, port,
NULL, NULL,
BT_IO_OPT_SOURCE_BDADDR, &device->src,
BT_IO_OPT_DEST_BDADDR, &device->dst,
BT_IO_OPT_CHANNEL, port->channel,
BT_IO_OPT_INVALID);
其中指定的连接成功后的回调函数为rfcomm_connect_cb。
在rfcomm_connect_cb中,调用了ioctl RFCOMMCREATEDEV。
此ioctl会在/dev目录下创建一个设备节点rfcomm%d。此设备节点的名称会被传给上层应用。这样,上层应用就能通过此节点发送、接收数据,其数据与远端设备之间通过蓝牙串口传输。