树莓派 2 上实现无缝串口蓝牙:基于 Windows 10 IoT Core
组件与物料
| 组件 | 数量 |
|---|---|
| 树莓派 2 Model B | 1 |
| 通用蓝牙 USB 适配器 | 1 |
| Arduino UNO | 1 |
| SparkFun BlueSMiRF Silver 蓝牙模块 | 1 |
应用与在线服务
- Microsoft Windows 10 IoT Core
- Microsoft Visual Studio 2015
项目详解
延伸阅读
本项目在 embedded101.com 有详细讨论:
"Win 10 IoT-Core: Bluetooth Universal Windows Serial App"
概述
这是一个通用 Windows 应用(UWP),将运行 IoT Core 的设备(如树莓派 2)通过通用蓝牙 USB 适配器,基于蓝牙串口配置文件(SPP)与嵌入式蓝牙设备连接。
背景知识
蓝牙支持短距离点对点网络。与 Wi-Fi 不同,蓝牙连接是一对一的。两设备可对称连接,通过串口配置文件传输原始数据。通常连接是不对称的:一端生成特定数据格式供另一端消费。例如手机实现 A2DP 向耳机推流音频;OBEX 是双向对称数据交换。
微软 IoT Core 官方蓝牙示例使用 GATT(通用属性配置文件),为蓝牙低功耗协议提供配置文件发现和描述服务,适合低功耗设备向复杂处理器发送数据。
本项目更简单,仅关注基于 RFCOMM 的通用 SPP(串口配置文件)。使用非常老旧的 USB 蓝牙适配器开发,因此应兼容任何蓝牙适配器。
IoT 应用
这是一个简单的蓝牙串口 UWP 测试应用。启动时枚举所有已配对的蓝牙设备列表,双击某项发起连接。在文本框输入要发送的文本,按按钮发送。应用自动接收并显示收到的文本。因此当终端设备(如下文 Arduino)只是回显收到的字符时,发送的文本会重现在 UWP 应用的接收框中。

终端设备
测试用 Arduino Uno + 蓝牙适配器。一个简单的扩展板,回显通过蓝牙收到的任何字符:
扩展板初始化代码:
// 串口回环应用
// 连接建立后由对端发起
// 注:蓝牙设备的 Rx/Tx 接 Arduino 的 Tx/Rx
// 编程时需断开蓝牙适配器
void setup() {
// 适配器波特率 115200(AT 命令模式下设置)
// 这是 Arduino 到蓝牙适配器的端口速率,非蓝牙空中速率
Serial.begin(115200);
delay(333);
while (!Serial);
delay(1000);
Serial.println("The quick brown fox jumps over the lazy dog!");
}
扩展板主循环:
void loop() {
char ch = Serial.read();
while (255 == (byte)ch) {
ch = Serial.read();
}
Serial.print(ch);
}
配对
蓝牙是点对点场景,连接前必须配对。配对不在应用内完成。嵌入式设备间用密钥配对可能有麻烦,因为它们常无头且不支持弹窗(IoT Core 也不支持)。可通过 IoT Core Web 门户或设备上的命令行工具(通过 SSH 运行)配对。最新版 OS Web 门户支持密钥配对(Win 10 RTM 时期缺失)。详见 "Win 10 IoT-Core: Bluetooth Universal Windows Serial App"。
创建项目
在 Visual Studio 2015 中使用通用 Windows 应用模板创建:
新建项目 → Visual C# → Windows → 空白应用(通用 Windows)
创建应用 UI
UI 用 XAML 实现,按上图布局创建:
- 所有文本元素为 TextBlocks,除了 SendText 是 TextBox
- SendText 接受回车(即多行)
- Recvd 文本启用 WrapText(多行)
- UI 元素用水平 StackPanel 组织成行,再用垂直 StackPanel 堆叠,连同 ListBox
- 也可用 Relative StackPanels 或 Grid 行列布局
ConnectDevices 是 ListBox,ItemsSource 绑定到 PairedDevices(见后),ItemTemplate 包含绑定到 PairedDevices Name 属性的 TextBlock。
<ListBox x:Name="ConnectDevices" ItemsSource="{Binding Source={StaticResource PairedDevices}}" Margin="10" DoubleTapped="ConnectDevices_DoubleTapped" Width="Auto" Height="Auto">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
应用配置
- 应用不使用 IoT Core 特性,无需扩展引用。
- 需在 Package.appxmanifest 添加蓝牙和串口特定功能。
- 双击该文件在解决方案资源管理器中的 UI 不支持添加这些功能,需右键选“打开方式 → XML 编辑器”手动添加。
修改底部 Capabilities 节为:
<Capabilities>
<Capability Name="internetClient" />
<DeviceCapability Name="bluetooth.rfcomm">
<Device Id="any">
<Function Type="name:serialPort"/>
</Device>
</DeviceCapability>
</Capabilities>
internetClient 功能默认已有,本应用不需要可移除,我保留了它。
已配对设备信息
应用启动时枚举已配对设备:
DeviceInformationCollection DeviceInfoCollection
= await DeviceInformation.FindAllAsync(RfcommDeviceService.GetDeviceSelector(RfcommServiceId.SerialPort));
将集合转为 ObservableCollection 供 ListBox 绑定:
_pairedDevices = new ObservableCollection<PairedDeviceInfo>();
遍历 DeviceInfoCollection,为每项创建 PairedDeviceInfo 对象加入 _pairedDevices。
PairedDeviceInfo 类属性:
Name <string>Id <string>DeviceInfo <DeviceInformation>
类有相应构造函数。DeviceInformation 即 Windows.Devices.Enumeration.DeviceInformation。
ListBox 显示绑定的 Name 字段。双击选中设备时,该项实际包含终端的 DeviceInformation,用于建立连接。
连接建立
用 DeviceInformation 获取 RFCOMM 服务,通过终端主机名(实际是蓝牙 MAC 地址)和服务名字符串建立 StreamSocket 连接:
_service = await RfcommDeviceService.FromIdAsync(DeviceInfo.Id);
_socket = new StreamSocket();
await _socket.ConnectAsync(_service.ConnectionHostName, _service.ConnectionServiceName);
若成功,socket 有意义(非 null)。当然有 try-catch 错误处理。
发送和接收文本使用 socket 的 InputStream 和 OutputStream 属性。
接收文本
接收文本应先启动,因为它作为独立线程运行。循环执行异步等待获取串口输入,处理后再次异步接收,直到取消令牌触发。
private async void Listen() {
ReadCancellationTokenSource = new CancellationTokenSource();
if (_socket.InputStream != null) {
dataReaderObject = new DataReader(_socket.InputStream);
while (true) {
await ReadAsync(ReadCancellationTokenSource.Token);
}
}
}
DataReader dataReaderObject;
private CancellationTokenSource ReadCancellationTokenSource;
private async Task ReadAsync(CancellationToken cancellationToken) {
uint ReadBufferLength = 1024;
cancellationToken.ThrowIfCancellationRequested();
dataReaderObject.InputStreamOptions = InputStreamOptions.Partial;
Task loadAsyncTask = dataReaderObject.LoadAsync(ReadBufferLength).AsTask(cancellationToken);
UInt32 bytesRead = await loadAsyncTask;
if (bytesRead > 0) {
string recvdtxt = dataReaderObject.ReadString(bytesRead);
}
}
发送文本
接收是异步的(到达时机不确定),发送由 UI 触发,本质同步(虽用 await 释放 UI)。
private async void Send(string msg) {
Task storeAsyncTask;
DataWriter dataWriteObject = new DataWriter(_socket.OutputStream);
dataWriteObject.WriteString(msg);
storeAsyncTask = dataWriteObject.StoreAsync().AsTask();
UInt32 bytesWritten = await storeAsyncTask;
}
按钮状态
命令按钮根据应用状态启用/禁用。例如未连接时 Send 和 Start Recv 禁用,连接后启用等。
Enjoy! :)
制造工艺
- 在树莓派 3 上扩展 GoPiGo v2 的 Windows 10 IoT Core 应用
- Arduino Mega 按键 PWM 调光:三阶段渐变 LED 实战教程
- 基于模拟传感器的高精度压力与气流测量装置
- Arduino Nano 与 MPU-6050 构建实时陀螺仪游戏
- ESP32-CAM 运动检测安防相机,推送照片到 Discord
- DHT11 温湿度传感器项目:LED 指示与压电蜂鸣器报警
- Unopad:基于 Arduino 的无缝 Ableton Live MIDI 控制器
- 自制 Arduino 电视遥控器:3D 打印外壳、电池供电、支持三大品牌协议
- Arduino + HC-05 蓝牙控制 LED:MIT App Inventor 手机 App 开发全攻略
- 自制简易红外避障传感器:IR LED + 光电二极管 + Arduino
- 语音控制家电:Google Assistant + 蓝牙 + Arduino UNO
- Windows 10 Cortana 蓝牙控制 Arduino LED:UWP 应用开发全流程