Shell中高效分割字符串

通常来说,使用 shell 脚本时候不需要太顾及程序的效率性,能完成任务,只要不是太慢,都可以接受。

但在一些特定应用场合,脚本效率也是一个硬性限制,不能耗时太久,比如说,用 shell 来编写一些处理 HTTP 请求的 CGI 程序,脚本的执行效率直接影响了 HTTP 的响应速度。

在 shell 中分割字符串有多种方法,比如利用 awk、cut 工具,还有内置方法,虽然效果相同,但是执行效率差距较大,尤其在处理大数据量上更是显著。

用 cut 工具分割字符串一般是以下用法:

1
2
# -f 指定列序号 -d 指定分隔符
echo $line | cut -f1 -d ' '

用 awk 工具分割字符串类似:

1
2
3
# $1 代表第一列 以此类推
# -F 指定分割符号
echo $line | awk -F ' ' {'print $1'}

内部方法分割字符串,用的是 IFS 变量和 set 命令:

1
2
3
4
5
6
# 设置分隔符
IFS=" "
# 分割 line
set -- $line
# $1 代表第一列,以此类推
echo $1

在 shell 脚本中,使用 awk/cut 这些工具在运行时会创建 sub-shell 来运行,相当于 C 语言中的 system/popen 调用,当需要使用的次数比较多时,这种调用会显著降低脚本的执行速度(创建进程是比较昂贵的)。

而使用内置方法则无需调用外部工具,执行起来效率更高,没有额外开销。

以处理 1000 行的空格分割的数字并分别累加为例,其文件内容大概如下:

1
2
3
4
5
6
7
8
1    25   30  100
2    26   33  102
3    27   36  104

...

999  1023 3024 2096
1000 1024 3027 2098

保存为文件 test.data。然后编写2个测试脚本,split1.sh 和 split2.sh。

split1.sh内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/bin/sh

DataFile="test.data"

if [ ! -e $DataFile ]; then
	echo "Fail to open file"
	exit 1
fi

F1=0
F2=0
F3=0
F4=0
# 设置分割符
IFS=" "

while read line
do
	[ -z "$line" ] && continue
	set -- $line

	F1=$((F1+$1))
	F2=$((F2+$2))
	F3=$((F3+$3))
	F4=$((F4+$4))
done < $DataFile

echo "F1=$F1"
echo "F2=$F2"
echo "F3=$F3"
echo "F4=$F4"

split2.sh文件内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/bin/sh

DataFile="test.data"

if [ ! -e $DataFile ]; then
	echo "Fail to open file"
	exit 1
fi

F1=0
F2=0
F3=0
F4=0
IFS=" "

while read line
do
	[ -z "$line" ] && continue
	l1=$(echo $line | cut -f1 -d ' ')
	l2=$(echo $line | cut -f2 -d ' ')
	l3=$(echo $line | cut -f3 -d ' ')
	l4=$(echo $line | cut -f4 -d ' ')

	F1=$((F1+$l1))
	F2=$((F2+$l2))
	F3=$((F3+$l3))
	F4=$((F4+$l4))
done < $DataFile

echo "F1=$F1"
echo "F2=$F2"
echo "F3=$F3"
echo "F4=$F4"

split1.sh使用内置方法,split2.sh使用 cut 工具来处理。在 shell 中运行程序并用 time 命令监控执行时间,会发现速度对比特别显著。

以本人测试电脑为例,内置方法执行输出如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$ time ./split1.sh
F1=500500
F2=524500
F3=1528500
F4=1099000

real	0m0.045s
user	0m0.024s
sys	0m0.020s

$ time ./split2.sh
F1=500500
F2=524500
F3=1528500
F4=1099000

real	0m4.603s
user	0m0.004s
sys	0m0.736s

粗略看出执行速度相差 100 倍左右。

备注:因为要获取各列数据累加,所以在脚本2中,使用了4次 cut 调用,相当于字符串分割了4此,而脚本1中只使用了一次字符串分割,这种策略也显著增加了执行开销。可以通过使用数组返回 cut 结果来避免多次调用,但是这得看 shell 环境是否支持,某些情况下是不支持数组的,比如 busybox 的 sh 环境。

(完)

Freescale Yocto 编译自定义 Linux 系统镜像

Yocto Project 是一个开源的协作软件,提供模板、工具和方法帮你创建定制的 Linux 系统和嵌入式产品,而无需关心硬件体系。适合嵌入式Linux开发人员使用。

下面来以 Freescale 的 i.MX6UL 开发板为例,详细叙述如何搭建 Yocto 环境以及 Linux 镜像的定制编译和烧录。

1. Freescale Yocto环境搭建

Yocto 需要一个 Linux 主机环境,可以通过搭建虚拟机来实现,发行版推荐 Ubuntu/Debian,其他主流发行版也可以。

如果通过虚拟机来搭建 Linux 环境,那么请注意分配足够的硬盘空间用来 Yocto 编译,推荐 100GB 以上。

这一节内容可以参阅 Freescale Yocto Project Main Page.

1.1 安装 Yocto 依赖包。

针对 Ubuntu/Debian 系统,可以使用如下命令安装所需要的包:

1
sudo apt-get install gawk wget git-core diffstat unzip texinfo  build-essential chrpath libsdl1.2-dev xterm curl

其他发行版可以参照此 链接 来安装依赖包。

1.2 下载 Freescale BSP 包

下载 Freescale 的板载支持包,Yocto 已经内置在包内。

这一小节内容可以参阅 FSL Community BSP.

下载 BSP 包需要 repo 工具,首先安装 repo 工具:

1
2
3
mkdir ~/bin
curl https://commondatastorage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo

备注:如果因为 GFW 的原因无法下载 repo,那么可以通过修改 hosts 文件来绕过:

1
2
3
4
# 也可以通过访问项目 https://github.com/racaljk/hosts 手动下载
curl https://raw.githubusercontent.com/racaljk/hosts/master/hosts > ~/hosts
# 请勿直接覆盖 /etc/hosts,否则会影响 apt-get 工具的使用
sudo cat ~/hosts >> /etc/hosts

下载 BSP 源码:

1
2
3
4
5
PATH=${PATH}:~/bin
mkdir fsl-community-bsp
cd fsl-community-bsp
repo init -u https://github.com/Freescale/fsl-community-bsp-platform -b jethro
repo sync

这一步会下载大量数据,完成后 Yocto 所需要的所有元数据已经齐备。

2. 镜像编译及自定义配置

现在已经可以编译 Linux 镜像了,以默认提供的 core-image-minimal 镜像配置为例:

1
source ./setup-environment build

这条命令用于初始化编译环境,其中 build 为指定的编译目录,也可以使用其他目录来当做编译目录。

编辑 build 目录下的 conf/local.conf 文件,指定开发板型号和配置并行编译参数:

1
2
3
MACHINE ?= "imx6ulevk"
BB_NUMBER_THREADS = "4"
PARALLEL_MAKE = "-j 4"

根据实际情况来配置,如果主机 CPU 核心数更多,可以将并行编译参数修改为 8.

备注:这里也可以使用图形化的工具 hob 而不是手动修改来指定开发板型号和其他配置, hob 可以输入命令 hob 启动。

编译 core-image-minimal 镜像,这是一个预定义的 Linux 系统镜像:

1
bitbake core-image-minimal

编译过程中会下载所有需要的包的源码,编译过程比较耗时,也会占用不少硬盘空间。

2.1 自定义配置

上面编译的是预定义的镜像,但在实际应用中,我们一般会希望能自定义镜像配置,比如添加去除一些组件,修改系统的设置等等。这一小节就来讲讲如何通过添加 Yocto Layer 来实现我们需要的自定义配置。

这一小节内容可以参阅 Yocto part III – a custom meta layer.

这里以添加一个自定义 Layer foobar 为例,并进行一些配置修改来说明自定义配置的一般方法。

1) 自定义 layer 镜像

切换到 fsl-community-bsp/sources 目录,使用命令添加一个 meta-foobar layer:

1
yocto-layer create foobar

命令提示一路回车确认即可。

添加完空 layer 后,在 meta-foobar 目录下,用如下命令创建目录树:

1
mkdir -p recipes-foobar/images

在新创建的目录下,创建一个 foobar-image.bb 文件,文件内容填写如下:

1
require recipes-core/images/core-image-minimal.bb

意思是我们以 core-image-minimal 为模板,给 foobar layer 添加了一个镜像目标,后续对镜像的自定义配置也是针对此镜像目标来操作,而不是直接去修改预定义的 core-image-minimal。

如果要编译 foobar 的镜像,切换到 build 编译目录下,并修改 conf/bblayers.conf 文件,按照已有的格式,将我们创建的 meta-foobar layer 添加到 BBLAYERS 变量中。

输入如下命令进行镜像编译:

1
bitbake foobar-image

2) 修改 busybox 配置

Busybox 是一个很有用的组件,提供了很多系统工具,但是默认并不激活所有工具,我们也要根据情况来激活/禁用某些工具(比如激活 telnetd)。

使用如下命令来进行 busybox 的图形化配置:

1
bitbake -c menuconfig busybox

根据界面提示和实际需要来进行配置操作。

使用如下命令来输出配置变更内容:

1
bitbake -c diffconfig busybox

保存路径设置为 ~/busybox.conf

在 meta-foobar 路径下创建目录树 recipes-core/busybox/files,将 busybox.conf 文件复制到此目录下保存。 然后,在 recipes-core/busybox 目录下,创建文件 busybox_%.bbappend 文件,文件内容填写如下:

1
2
FILESEXTRAPATHS_prepend := "${THISDIR}/files:"
SRC_URI += "file://busybox.cfg"

重新编译 foobar-image,这样我们自定义的 busybox 配置就生效了。

从这里可以看出,如果我们要修改一个 Yocto 自带的一个组件,那么我们就通过添加 layer,同时使用相同的 recipes 路径配合 bbappend 文件,来对默认的配置进修改即可。(bbappend 文件名中的 % 符号是版本号的占位符)

3) 修改默认系统名

Linux 系统名是由 sources/poky/meta/recipes-core/base-files/ 下的 base-files_3.0.14.bb 文件控制的,那么我们通过添加对应的 bbappend 文件来进行配置。

在 meta-foobar layer 路径下创建目录树 recipes-core/base-files,并在目录下创建 base-files_%.bbappend 文件,文件内容如下:

1
hostname = "foobar"

这样子就修改了默认系统名为 foobar。

4) 修改 root 默认密码

修改 root 密码、添加用户,可以参考链接:FAQ:How do I set or change the root password.

修改 recipes-foobar/images/foobar-image.bb 文件,添加如下代码:

1
2
inherit extrausers
EXTRA_USERS_PARAMS = "usermod -P 123456 root;"

其中 123456 即为设置的 root 密码。

备注:如果要使 root 密码生效,需要修改 build 目录下的 conf/local.conf 文件,注释掉 EXTRA_IMAGE_FEATURES = "debug-tweaks 这个选项。

5) 修改 sshd 默认配置

sshd 提供 ssh 登录服务,但是默认配置下不允许 root 进行登录,默认也不允许密码验证,在实际使用中并不方便(方便也意味这安全隐患),要允许 root 以密码验证方式登录,可以使用如下方法修改。

sshd 组件的默认配置保存在 recipes-connectivity/openssh/openssh 路径下的 sshd_config 文件中,允许 root 登录是由选项 PermitRootLogin 控制,允许密码验证是由选项 PasswordAuthentication 控制,我们看到这两项默认被注释掉了。

要修改此配置,我们要在 meta-foobar 下创建目录树 recipes-connectivity/openssh/files,并将默认的 sshd_config 文件复制一份到此目录下,并取消 PermitRootLogin yes 和 PasswordAuthentication yes 的两行注释。

然后在目录 recipes-connectivity/openssh 下创建 openssh_%.bbappend 文件,内容如下:

1
FILESEXTRAPATHS_prepend := "${THISDIR}/files:"

这样子我们修改过的 sshd_config 文件配置会覆盖默认配置。

6) 修改默认时区

如果不修改,默认编译的系统镜像,时区设置的是 UTC 时区,通常我们想修改为当地时区,比如中国为 PRC 时区。

时区组件配置由 recipes-extended/tzdata/tzdata_2016e.bb 文件中的 DEFAULT_TIMEZONE 来控制。

我们在 meta-foobar 下创建目录树 recipes-extended/tzdata,并在目录下创建 tzdata_%.bbappend 文件,内容如下:

1
DEFAULT_TIMEZONE = "PRC"

这样就将默认时区设置为中国。

以上 1-6 就详细叙述了常见的配置修改,修改模式基本都是通过自定义的配置去覆盖默认配置实现,用 bbappend 文件去调整默认配置,而不是直接去修改默认配置文件。

3. 镜像的烧录

编译的镜像都存放在 tmp/deploy/images 下,针对 SD 卡的镜像为 sdcard 或 sdcard.gz 结尾的文件(gz 文件需要解压后烧录)。

在 linux 环境下,可以使用如下命令来烧录镜像到 SD 卡中:

1
sudo dd if=foobar-image-xxx.sdcard of=/dev/sdb

在 Windows 上,可以使用 USB Image Tool 来烧录镜像。

需要注意的是,Yocto 默认编译生成的镜像只占用 SD 卡上的一个小分区,如果想让系统使用全部的 SD 卡空间,需要使用工具来调整。这里推荐使用 GParted 工具的 resize/move 功能,此工具为图形化工具,非常方便。

GParted 安装:

1
sudo apt-get install gparted

GParted 运行:

1
sudo gparted

(完)

32feet.net 蓝牙虚拟串口编程

Windows 上的蓝牙编程并不方便,由于操作系统并没有提供统一的蓝牙操作接口,通常是由各个蓝牙设备商提供蓝牙栈,所以要想编程兼容这些不同的厂商蓝牙是一个问题。好在有个软件项目 32feet.NET 针对主流蓝牙协议栈提供了支持,包括 Microsfot, Widcomm, BlueSolei 等,同时还支持红外传输协议。

蓝牙虚拟串口是一个较为常见的需求,为了兼容已有使用串口设备的程序,需要将蓝牙连接转为系统上的虚拟串口,然后提供给其他程序或库使用。32feet.NET 对此也提供了支持。

32feet.NET 依赖 .NET 3.5 版本以上框架,支持 Windows 桌面版本、Wndows CE 以及 Windows Phone。

安装

如果使用 Visual Studio 2015 或者 安装有 NuGet 工具的,可以直接通过 NuGet 安装。在 NuGet 命令行中输入

1
Install-Package 32feet.NET

这样 NuGet 会自动下载安装并添加到当前 .NET 项目中。可以检查项目 References 项,如果存在 InTheHand.Net.Personal 则表明已成功加入到项目中,如果没有,可以手动添加引用,NuGet 下载存放在 SolutionName\packages\32feet.NET.x.x.x.x\ 路径下。

使用

下面主要讲解 32feet.NET 的使用,覆盖蓝牙搜索、配对、直接连接以及虚拟串口服务。

1. 检测系统蓝牙可用性

如果系统没有蓝牙设备或者蓝牙设备被禁用,那么可以通过以下函数来进行检查:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* 定义于类 BTHelper 中 */
public static bool IsPlatformSupportBT()
{
    BluetoothClient bc;
    try
    {
        bc = new BluetoothClient();
    }
    catch (PlatformNotSupportedException)
    {
        return false;
    }
    bc.Close();
    return true;
}

如果系统不支持蓝牙设备,那么 new BluetoothClient 会抛出 PlatformNotSupportedException 异常,通过捕获这个异常,来检测系统蓝牙设备可用性。

2. 蓝牙搜索

BluetoothClient 对象有个方法 DiscoverDevices 用于搜索蓝牙设备,该方法有多个重载版本,最终都是调用如下这个接口:

1
public BluetoothDeviceInfo[] DiscoverDevices(int maxDevices, bool authenticated, bool remembered, bool unknown, bool discoverableOnly);

第一个参数表明搜索的最大设备数,第二个参数表示是否搜索已配对设备,第三个表示是否搜索记住的设备,第四个表示是否搜索未知设备,第五个参数表示是搜索范围内可被发现的设备。

这里面重要的是第二和第五个参数,第二个代表搜索系统中已配对列表中的设备,即使它们现在并不在线。第五个参数 XP 系统上不支持,表示搜索范围内可被发现的设备。

如果我们需要获得系统中已配对列表中的蓝牙设备,可以这样调用 DiscoverDevices(255, true, false, false),这里使用了4个参数的重载版本,只将第二个参数置为 true。

如果我们需要搜索周围环境中可用的蓝牙设备,可以这样调用 DiscoverDevices(255, false, false, false, true),这个调用等同于 DiscoverDevicesInRange()。

需要注意的是,这个调用是同步阻塞的,在搜索没有结束之前函数不会返回。所以通常我们需要将这个调用放入工作线程中。例外的是,如果只是获取系统中已配对列表中的设备,这个调用会很快完成,不会占用当前线程太多时间。

库中同时提供了一个异步搜索方法,由类 BluetoothComponent 提供,由事件 DiscoverDevicesProgress 和 事件 DiscoverDevicesComplete 以及方法 DiscoverDevicesAsync 来实现。这个和用 BackgroundWorker 来实现 DiscoverDevices() 异步查找是一样的。

PS: 可以通过 VS 的 Object Browser 查看 InTheHand.Net.Personal 库中的函数接口说明,说明非常翔实。

3. 检测蓝牙设备是否在范围内

当需要检查一个蓝牙设备是否在有效范围内,可以通过查询一个 Fake Service ID 来实现。

如果蓝牙设备在范围内可访问,那么查询的结果是返回的服务记录为空,表示不支持此服务;如果蓝牙设备不在范围内,那么会抛出套接字异常。

以下为检测代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* 定义于类 BTHelper 中 */
public static bool TestingIfInRange(BluetoothAddress addr)
{
    bool inRange = false;
    Guid fakeUuid = new Guid("{F13F471D-47CB-41d6-9609-BAD0690BF891}");
    BluetoothDeviceInfo device = new BluetoothDeviceInfo(addr);

    try
    {
        ServiceRecord[] records = device.GetServiceRecords(fakeUuid);
        Debug.Assert(records.Length == 0, "Why are we getting any records? len: " + records.Length);
        inRange = true;
    }
    catch (SocketException)
    {
        inRange = false;
    }
    return inRange;
}

需要注意的是,因为蓝牙设备通信需要时间,所以这个调用也需要较长时间才能完成,未完成之前进入阻塞不返回,所以这个方法也需要放入工作线程中执行。

这部分内容可以参考官方文档: Tesing if a device is in rage.

4. 蓝牙设备配对

蓝牙配对功能由类 BluetoothSecurity 的静态方法 PairRequest(BluetoothAddress device, string pin) 提供,其中第一个参数是目标设备地址,第二个参数是用于配对的 Pin 码。配对成功返回 true,失败返回 false。

解除配对是 BluetoothSecurity.RemoveDevice(BluetoothAddress device) 。

配对操作也需要将长时间完成,所以这个方法也需要放入工作线程中执行。

5. 蓝牙直接连接

如果不需要蓝牙虚拟串口而直接读写蓝牙数据,那么可以使用直接连接,这个可以参照官方文档:General Bluetooth Data Connections.

通过直接连接获取一个可读写的 System.IO.Stream 流对象,就可以直接对蓝牙进行读写数据操作了。以下为官方样例中的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
BluetoothAddress addr = BluetoothAddress.Parse("001122334455");
Guid serviceClass;
serviceClass = BluetoothService.SerialPort;
// - or - etc
// serviceClass = MyConsts.MyServiceUuid
//
var ep = new BluetoothEndPoint(addr, serviceClass);
var cli = new BluetoothClient();
cli.Connect(ep);
Stream peerStream = cli.GetStream();
// peerStream.Write/Read ...

6. 蓝牙虚拟串口

这部分讲解如何实现将蓝牙连接转为系统上的虚拟串口并获取串口名,还可以参阅官方文档: Bluetooth Serial Ports.

首先需要说明的是,虚拟串口和直接连接数据读写不能同时使用,如果已经使用了其中一个方法进行读写,那么另外的一个方法会失败。

生成虚拟串口需要将蓝牙设备服务设置为 BluetoothService.SerialPort,官方样例代码如下:

1
2
3
4
BluetoothAddress addr = BluetoothAddress.Parse("123456789012");
BluetoothDeviceInfo device = new BluetoothDeviceInfo(addr);  // Or from discovery etc
bool state = true;
device.SetServiceState(BluetoothService.SerialPort, state, true);

SetServiceState 的第一个函数表示服务标识,第二个表示要设置的服务状态,true 为启用, false 为禁用,第三个参数表示如果设置失败是否抛出异常。

但在实际使用中,某些双模蓝牙在设置时,虽然设置成功但是依然会抛出 Win32Exception 异常,所以这里建议调用此函数的重载版本 SetServiceState(System.Guid service, bool state),而想要知道是否设置成功,下面会介绍其他方法来获得。

这个方法调用并不会告诉我们新生成的串口名,官方文档中的建议是通过设置前后的系统串口列表差异来获取新生成的串口名,而获取系统串口列表则可以调用静态方法 SerialPort.GetPortNames。

但是不建议用这个方法来获取虚拟串口名,因为这个不可靠而且容易出错,下面介绍另外一个可靠方法,而且这个方法也可以同时告诉我们设置虚拟串口服务是否成功。

获取蓝牙虚拟串口名

这部分参阅官方文档:Getting Virtual COM Port Names for Remote Bluetooth Devices.

蓝牙虚拟串口在系统中都有记录,我们可以通过检索这个记录,来找到所设置的设备的虚拟串口名,同时也可以得知我们的蓝牙虚拟串口是否设置成功。

通过调用 WMI 查询,可以枚举出系统中每个串口的详细信息。这里可以通过 PowerShell 来查询:

1
C:\> Get-WmiObject -query "select DeviceID,PNPDeviceID from Win32_SerialPort"

输出样例(其中 COM66 对应的蓝牙设备地址为 00803A686519):

1
2
3
4
DeviceID     : COM66
PNPDeviceID  : BTHENUM\{00001101-0000-1000-8000-00805F9B34FB}\7&1D80ECD3&0&00803A686519_C00000003

......

可以看出蓝牙虚拟串口的 PNPDeviceID 是以 BTHENUM 开头,并且会将蓝牙地址存放其中。

这个查询也可以通过 C# 代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
using System.Management;

const string Win32_SerialPort = "Win32_SerialPort";
SelectQuery q = new SelectQuery(Win32_SerialPort);
ManagementObjectSearcher s = new ManagementObjectSearcher(q);
foreach (object cur in s.Get()) {
    ManagementObject mo = (ManagementObject)cur;
    object id = mo.GetPropertyValue("DeviceID");
    object pnpId = mo.GetPropertyValue("PNPDeviceID");
    console.WriteLine("DeviceID:    {0} ", id);
    console.WriteLine("PNPDeviceID: {0} ", pnpId);
    console.WriteLine("");
}//for

综上,我们可以定义一个函数,这个函数通过 WMI 检索所有串口设备信息,然后返回一个 Hashtable,其中存储的键为蓝牙设备地址,存储的值为串口名。

函数定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* 定义于类 BTHelper 中 */
public static Hashtable QueryBTHPorts()
{
    const string Win32_SerialPort = "Win32_SerialPort";
    SelectQuery q = new SelectQuery(Win32_SerialPort);
    ManagementObjectSearcher s = new ManagementObjectSearcher(q);

    Hashtable hashResult = new Hashtable();
    foreach (object cur in s.Get())
    {
        ManagementObject mo = (ManagementObject)cur;
        string id = mo.GetPropertyValue("DeviceID").ToString();
        string pnpId = mo.GetPropertyValue("PNPDeviceID").ToString();
        Debug.WriteLine("WMI>>DeviceID: " + id);
        Debug.WriteLine("WMI>>PNPDeviceID: " + pnpId);
        Debug.WriteLine("");

        /* 仅处理蓝牙串口 */
        if (pnpId.StartsWith("BTHENUM"))
        {
            /* 从 PNPDeviceID 中提取出蓝牙地址,策略是逆序字符串  & 后 _ 之前 */
            /* 蓝牙地址为6字节,HEX为12位字符 */
            int rBound = pnpId.LastIndexOf('_');
            int lBound = pnpId.LastIndexOf('&');
            Debug.Assert(rBound - lBound == 13, "Get BT Addr, this will nevery happened.");
            string addr = pnpId.Substring(lBound + 1, 12);
            if (!hashResult.Contains(addr))
            {
                hashResult.Add(addr, id);
            }
            else
            {
                Debug.WriteLine("Get BT Addr, addr" + addr + " has more than 1 ports");
            }
        }
    }

    return hashResult;
}

同样,WMI 检索也比较耗时,所以这个函数调用需要放入工作线程中执行。

有了这个函数,我们可以通过结果中查找我们设置的蓝牙设备地址,就可以得知对应的虚拟串口名,而如果结果中没有我们设置的蓝牙设备地址,那么就可以认定设置虚拟串口服务失败了。

样例代码

这里介绍通过配合使用 BackgroundWorker 来实现蓝牙设备的配对、生成虚拟串口的样例代码:

我们通过传入蓝牙设备地址给 worker,worker 在 DoWork 中帮我们处理所有步骤,过程中报告进度,并最终告诉我们结果。

worker 的 DoWork 事件代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/* 进行蓝牙连接操作的后台线程执行函数 */
/* 传入 蓝牙地址字符串 作为参数 */
/* 利用 ReportProgress 报告进度 */
/* 利用 DoWorkEventArgs.Result 报告状态 */
void bgworkerConnection_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker worker = sender as BackgroundWorker;
    string args = e.Argument as string;
    string finalportname = "";

    do
    {
        BluetoothAddress addr = BluetoothAddress.Parse(args);
        BluetoothDeviceInfo deviceinfo = new BluetoothDeviceInfo(addr);

        /* 检测设备是否在服务范围内,有些设备能被发现但不能被配对连接(如果已经被其他终端配对连接) */
        /* 不在范围内则报告错误信息 */
        worker.ReportProgress(10, "Testing if device in range...");
        if (!BTHelper.TestingIfInRange(addr))
        {
            string msg = string.Format("Error: Device {0}({1}) is not reachable!",
                    deviceinfo.DeviceName,
                    deviceinfo.DeviceAddress.ToString());
            e.Result = msg;
            return;
        }

        worker.ReportProgress(10, "Device is in range.");

        /* 检测设备是否是已配对设备 */
        if (deviceinfo.Authenticated)
        {
            /* 已配对设备枚举系统蓝牙串口,检测是否已绑定虚拟串口 */
            worker.ReportProgress(10, "Querying Bluetooth serialport...");
            Hashtable bthTable1 = BTHelper.QueryBTHPorts();
            if (bthTable1.ContainsKey(addr.ToString()))
            {   /* 获取绑定串口 */
                finalportname = bthTable1[addr.ToString()] as string;
                /* !!! 跳出 !!! */
                break;
            }
            /* 此处 else 需考虑枚举结果中没有绑定串口的处理,如果没有绑定串口,那么重新设置串口服务 */
            /* 此处 else 部分会在下面的流程处理,见下方设置串口服务部分 */
        }
        else /* 需要进行配对 */
        {
            worker.ReportProgress(10, "Start pairing...");
            if (!BluetoothSecurity.PairRequest(addr, "1234"))
            {  /* 配对失败 */
                string msg = string.Format("Error: Can not pair to Device {0}({1})!",
                        deviceinfo.DeviceName,
                        deviceinfo.DeviceAddress.ToString());
                e.Result = msg;
                return;
            }
        }

        /* 设置串口服务 */
        /* SLC蓝牙设置串口服务时,如果 SetServiceState */
        /* 第三个参数为 true (允许异常) 那么则一定会抛出异常,即使虚拟串口创建成功 */
        /* 所以这里调用2个参数的重载版本 */
        /* 之后检查串口服务是否设置成功的方法就是 设置后检索蓝牙串口 */
        worker.ReportProgress(10, "Enable serialport service...");

        deviceinfo.SetServiceState(BluetoothService.SerialPort, true);

        /* 枚举系统蓝牙串口,检查对应蓝牙是否有串口绑定 */
        Hashtable bthTable2 = BTHelper.QueryBTHPorts();
        if (bthTable2.ContainsKey(addr.ToString()))
        {
            /* 获取绑定的串口 */
            finalportname = bthTable2[addr.ToString()] as string;
            /* !!! 跳出 !!! */
            break;
        }
        else
        {   /* 启用串口服务失败或者无法检测到绑定串口 */
            string msg = string.Format("Error: Failed to set Serialport service for Device {0}({1})!",
                    deviceinfo.DeviceName,
                    deviceinfo.DeviceAddress.ToString());
            e.Result = msg;
            return;
        }

    } while (false);

    worker.ReportProgress(10, "Open serialport...");

    Debug.WriteLine(string.Format(">>PORT NMAE: {0}", finalportname));

    /* 成功 */
    /* 将串口名放入消息中返回 */
    string okmsg = string.Format("OK: {0}", finalportname);
    e.Result = okmsg;
}

代码中通过灵活使用 string 作为 Result 来表明执行结果。

worker 的 RunCompleted 事件代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void bgworkerConnection_RunCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    string msg = e.Result as string;
    if(msg.StartsWidth("OK"))
    {   /* OK */
        MessageBox.Show(msg);
    }
    else
    {  /* Error */
        MessageBox.Show(msg);
    }
}

后续可以通过字符串处理提取出生成的串口名。

worker 的启动代码:

1
bgworkerConnection.RunWorkerAsync(addrstring);

这样子我们就获取到了绑定的虚拟串口名,但是在使用串口中要注意的是,虚拟串口打开过程中,本质上还是发起了一个蓝牙数据连接,所以打开过程会耗时较长,所以这个虚拟串口打开过程也需要放入工作线程中处理。如果虚拟串口对应的蓝牙设备没有在线或者其他原因造成不可用,那么打开时会抛出异常,需要在代码上进行处理。

一旦蓝牙设备经过绑定并且设置虚拟串口服务成功,那么对应的串口会一直存在与系统的串口列表中,但这并不代表它是可用的,请在打开这类串口时添加异常处理代码。

(完)