最近在开发智能硬件项目时,我遇到了一个典型痛点:如何让没有屏幕的IoT设备快速接入Wi-Fi网络?传统方案要么依赖手机热点切换,要么需要硬件增加额外按键,用户体验都不够优雅。经过技术调研,发现采用Improv Wi-Fi蓝牙配网协议是个不错的解决方案,于是决定用.NET技术栈实现一套跨平台的配网工具。
Improv Wi-Fi协议的核心思想是通过蓝牙BLE通道传输Wi-Fi凭证。设备启动后进入配网模式,通过广播特定服务UUID告知客户端自己的存在。手机或电脑上的控制端扫描到设备后,建立蓝牙连接并将目标Wi-Fi的SSID和密码加密传输给设备。设备获取凭证后自动尝试连接指定网络,并通过蓝牙回传连接状态。
这套方案相比传统配网方式有三大优势:
系统需要实现两端:
考虑到.NET的跨平台特性,我选择如下技术栈:
根据官方规范,需要实现以下核心服务:
| 服务UUID | 特性UUID | 说明 |
|---|---|---|
| 00467768-6228-2272-4663-277478268000 | 00467768-6228-2272-4663-277478268001 | 设备状态 |
| 00467768-6228-2272-4663-277478268000 | 00467768-6228-2272-4663-277478268002 | 错误码 |
| 00467768-6228-2272-4663-277478268000 | 00467768-6228-2272-4663-277478268003 | RPC命令 |
| 00467768-6228-2272-4663-277478268000 | 00467768-6228-2272-4663-277478268004 | RPC结果 |
设备状态机需要处理以下状态转换:
code复制配网就绪 → 凭证接收中 → 凭证接收完成 → 正在连接 → 连接成功/失败
Wi-Fi凭证传输需要特别注意安全性:
设备端以ESP32为例,开发环境配置步骤:
csharp复制// 初始化蓝牙
BluetoothLEServer server = BluetoothLEServer.Instance;
server.DeviceName = "ImprovDevice";
GattServiceProvider improvService = CreateImprovService();
// WiFi配置
WifiNetworkHelper.Connect(ssid, password, requiresDateTime: true);
核心服务构建代码示例:
csharp复制private GattServiceProvider CreateImprovService()
{
var serviceProvider = new GattServiceProvider(
new Guid("00467768-6228-2272-4663-277478268000"));
// 状态特征
var statusChar = new GattLocalCharacteristic(
new Guid("00467768-6228-2272-4663-277478268001"),
new GattLocalCharacteristicParameters {
CharacteristicProperties = GattCharacteristicProperties.Read |
GattCharacteristicProperties.Notify,
UserDescription = "Device State"
});
// RPC命令特征
var rpcCmdChar = new GattLocalCharacteristic(
new Guid("00467768-6228-2272-4663-277478268003"),
new GattLocalCharacteristicParameters {
CharacteristicProperties = GattCharacteristicProperties.Write,
UserDescription = "RPC Command"
});
serviceProvider.AddCharacteristic(statusChar);
serviceProvider.AddCharacteristic(rpcCmdChar);
return serviceProvider;
}
设备端需要维护当前状态并处理客户端指令:
csharp复制enum ImprovState {
ReadyToProvision = 1,
Provisioning = 2,
Provisioned = 3,
Connecting = 4,
Connected = 5
}
void ProcessRpcCommand(byte[] command)
{
switch(command[0]) {
case 0x01: // 设置凭证
currentState = ImprovState.Provisioning;
string ssid = Encoding.UTF8.GetString(command, 1, command[1]);
string password = Encoding.UTF8.GetString(command, 2+command[1], command[2+command[1]]);
StartConnectWiFi(ssid, password);
break;
case 0x02: // 获取当前状态
SendStateUpdate();
break;
}
}
MAUI项目中需要处理各平台蓝牙权限:
xml复制<!-- Android Manifest -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
使用Plugin.BLE库简化跨平台操作:
csharp复制var adapter = CrossBluetoothLE.Current.Adapter;
var devices = await adapter.ScanForDevicesAsync(new Guid("00467768-6228-2272-4663-277478268000"));
var device = devices.First();
await adapter.ConnectToDeviceAsync(device);
var service = await device.GetServiceAsync(improvServiceUuid);
var stateChar = await service.GetCharacteristicAsync(stateCharUuid);
var rpcChar = await service.GetCharacteristicAsync(rpcCharUuid);
MAUI页面关键元素:
XAML示例:
xml复制<VerticalStackLayout>
<Button Text="扫描设备" Command="{Binding ScanCommand}"/>
<ListView ItemsSource="{Binding Devices}">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Name}" Detail="{Binding Id}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Entry Placeholder="Wi-Fi名称" Text="{Binding Ssid}"/>
<Entry Placeholder="密码" Text="{Binding Password}" IsPassword="True"/>
<Button Text="发送凭证" Command="{Binding ProvisionCommand}"/>
<Label Text="{Binding StatusMessage}"/>
<ProgressBar Progress="{Binding Progress}"/>
</VerticalStackLayout>
处理与设备的交互流程:
csharp复制async Task ProvisionDevice(string ssid, string password)
{
// 构建RPC命令
var ssidBytes = Encoding.UTF8.GetBytes(ssid);
var pwdBytes = Encoding.UTF8.GetBytes(password);
var command = new byte[3 + ssidBytes.Length + pwdBytes.Length];
command[0] = 0x01; // 设置凭证指令
command[1] = (byte)ssidBytes.Length;
Array.Copy(ssidBytes, 0, command, 2, ssidBytes.Length);
command[2 + ssidBytes.Length] = (byte)pwdBytes.Length;
Array.Copy(pwdBytes, 0, command, 3 + ssidBytes.Length, pwdBytes.Length);
// 发送命令
await rpcChar.WriteAsync(command);
// 监听状态变化
stateChar.ValueUpdated += (o, args) => {
var state = (ImprovState)args.Characteristic.Value[0];
UpdateUI(state);
};
await stateChar.StartUpdatesAsync();
}
设备无法被发现
凭证传输失败
Wi-Fi连接超时
蓝牙广播间隔调整:
csharp复制// ESP32上设置广播间隔为100ms
var advParams = new AdvertisementParameters {
Interval = 100,
Type = AdvertisementType.ConnectableUndirected
};
server.StartAdvertising(advParams);
数据包压缩:
内存优化:
设备认证:
日志记录:
OTA支持:
在实际部署中,我发现这套基础架构可以进一步扩展:
批量配网模式:
在工厂生产时,可以通过NFC写入初始配置,设备首次上电自动连接测试工位的Wi-Fi
多协议支持:
除了Improv协议,可以同时实现Apple HomeKit的EAP配网方式,提升iOS用户体验
网络诊断功能:
设备连接Wi-Fi后,可以通过蓝牙通道上报网络质量数据(信号强度、丢包率等)
声波配网备用方案:
在蓝牙不可用时,可以通过扬声器播放编码后的Wi-Fi信息,手机麦克风接收解码
这套.NET实现的跨平台配网方案已经在我们的智能插座产品线上稳定运行,平均配网时间从原来的2分钟缩短到15秒以内,用户投诉率下降了83%。对于需要连接Wi-Fi的无屏设备,Improv协议确实是个优雅的解决方案。