用一套协议层,同时跑通 Windows 和 Linux 的 BLE 配网流程,使用.NET 轻松实现嵌入式设备的首次联网引导。通过 NuGet 轻松集成到自己的项目里,支持 AOT 场景。
1. 前言
做嵌入式设备或者物联网设备时,配网几乎是绕不过去的一步。设备出厂后,用户总得先把它接到 Wi-Fi 上,后面的远程控制、设备发现、网页配置,才谈得上真正可用。
而 Improv Wi-Fi 正是为这件事准备的一套开放协议。它定义了设备如何通过 Bluetooth LE 或 Serial 接收 Wi-Fi 凭据,并在联网成功后返回一个 URL,让客户端继续完成后续配置。这套协议的边界很清晰,所以特别适合做"首次接入"这件事。当然,如果你的设备有经常移动更换网络的需求,那么这个配网方案也完全可以被复用。
我前面其实已经写过很多篇和 Improv 相关的文章,主要是介绍的使用.NET NanoFramework 在 ESP32 上实现设备端配网。
不过它们更多是围绕具体硬件项目展开。这一次我想单独聊聊我整理出来的这个 .NET 版本实现:Sang.ImprovWifi。它的目标不是只做一个能跑的示例,而是把协议层和平台层拆开,让 Windows 和 Linux 共用同一套核心逻辑。
这篇文章也会把重点放在"怎么用"上,主要回答两个问题:
- 如果你只是想快速验证
Improv Wi-Fi流程,怎么直接跑仓库里的 Demo - 如果你已经有自己的
.NET项目,怎么通过 NuGet 包接入,尤其是在 Linux 场景下使用ImprovWifi.Transport.Linux.Aot
官方资料放在这里,感兴趣的话可以先看一眼:
2. 这个项目解决了什么问题
如果只是临时做一个蓝牙配网服务,自己定义几个 Characteristic,约定一套数据格式,通常也能很快跑起来。
但这种方式有一个很明显的问题:协议、平台 API 和业务逻辑很容易写死在一起。等你后面想换平台、换蓝牙库,或者只是想把协议逻辑单独拿出来复用时,成本就会一下子高起来。
我做 Sang.ImprovWifi 时,最在意的就是不要把这几层揉成一团。所以现在这个仓库的设计目标很明确:
- 用纯协议层处理
Improv的编解码、状态与事件 - 用平台宿主层分别对接
Windows和Linux的 BLE 实现 - 用最小 Demo 把整条配网链路跑通,方便联调和二次开发
目前它已经可以完成下面这些事情:
- 接收并解析
Improv的 Wi-Fi 配网命令 - 处理
Identify、授权状态、错误状态和能力值读取 - 在配网成功后生成并发送
URL类型的RPC Result - 在
Windows上作为真实的 BLE 外设运行 - 在
Linux / BlueZ上作为真实的 BLE 外设运行
如果你正在做的是设备端配网、主机侧配网服务,或者只是想在 .NET 下完整跑一遍 Improv Wi-Fi 协议流程,这个仓库基本已经能拿来直接用。
3. 仓库里都有什么
当前仓库主要包含下面几个项目:
ImprovWifi.ProtocolImprovWifi.Transport.WindowsImprovWifi.Transport.LinuxImprovWifi.Transport.Linux.AotImprovWifi.Demo.WindowsImprovWifi.Demo.LinuxImprovWifi.Demo.Linux.Aot
其中最核心的是 ImprovWifi.Protocol。这一层不依赖 Windows 或 Linux 的蓝牙 API,里面主要包含:
ImprovServiceImprovPacketCodecImprovTransportServerImprovTransportOptions- 一组和协议状态相关的事件类型
平台层要做的事情,其实就是把各自的 BLE 读写动作接到协议层上。比如把客户端写入 RpcCommand 的数据交给 ImprovService,再把它生成的状态和结果同步到对应特征值中。
也正因为这样,Windows 和 Linux 的差异被限制在了宿主层,协议层本身不需要跟着平台变化一起改。
4. 最快的使用方式:直接跑仓库 Demo
如果你现在只是想验证整个配网流程,我建议先从仓库里的 Demo 开始。这样最直接,也最容易确认到底是代码问题、蓝牙环境问题,还是客户端工具的问题。
仓库地址:
先在仓库根目录构建:
bash
dotnet build
4.1 运行 Windows Demo
bash
dotnet run --project ImprovWifi.Demo.Windows/ImprovWifi.Demo.Windows.csproj
运行前需要确认:
- 蓝牙已开启
- 适配器支持
Peripheral Role - 系统允许本地
GATT Service注册
Windows Demo 的默认行为很简单:
- 启动后输入一个
Redirect URL - 等待支持
Improv的客户端扫描到设备 - 收到
Identify请求后自动授权 - 收到 Wi-Fi 凭据后输出
SSID和Password - 调用
CompleteProvisioning(url)返回结果地址

它的核心处理逻辑大致就是下面这样:
csharp
using ImprovWifi.Protocol;
using ImprovWifi.Transport.Windows;
var server = new WindowsImprovServer(new WindowsImprovServerOptions
{
DeviceName = "improv-windows-demo",
AdapterName = "default",
AutoAuthorize = false
});
server.IdentifyRequested += (_, _) =>
{
Console.WriteLine("identify requested");
server.Authorize(true);
};
server.ProvisioningRequested += (_, e) =>
{
Console.WriteLine($"ssid={e.Ssid}, password={e.Password}");
server.CompleteProvisioning("http://192.168.1.10/");
};
server.Start();
Console.ReadLine();
server.Stop();
这个示例没有真正去连接系统 Wi-Fi,它只是把协议流程跑通。真实项目里,你通常会在 ProvisioningRequested 事件中接入自己的联网逻辑。
4.2 运行 Linux Demo
bash
dotnet run --project ImprovWifi.Demo.Linux/ImprovWifi.Demo.Linux.csproj -f net10.0
运行前需要确认:
- 系统已安装.Net 10.0
- 系统已安装并启动
BlueZ bluetoothd正常运行- 可以通过
bluetoothctl看到蓝牙适配器,例如hci0 - 当前用户具备蓝牙访问权限
Linux 版当前使用的是基于 BlueZ 的宿主实现,直接对接系统蓝牙栈。运行流程与 Windows 版一致,也是先广播、接收识别请求、接收配网信息,然后返回 RPC Result URL。
如果你是在 WSL2 里跑,需要额外确认蓝牙透传本身已经成立。否则即便代码没有问题,外围设备广播和 GATT Server 注册也可能无法真正工作。
5. 如何集成到自己的项目里
跑通 Demo 之后,接下来的问题通常就变成了:我能不能不直接改仓库里的 Demo,而是把它接到自己的项目里?答案当然是可以。
5.1 NuGet 包直接引用
如果你更关心"在自己的程序里快速挂一个配网服务",那直接引用 NuGet 包会更方便。

这里可以使用这几个包:
如果你是在 Windows 上开发,直接引用 ImprovWifi.Windows 就可以了;如果你是在 Linux 上开发,推荐直接上 ImprovWifi.Transport.Linux.Aot 这个包。后者虽然名字里带了 Aot,但它的 API 和 Linux 包是完全一样的,只不过底层实现更适合 AOT 场景。底层通过 LibraryImport 调用原生实现,更适合需要 NativeAOT 或更轻量运行方式的场景。
下面是一段最小可用的示例代码:
csharp
using ImprovWifi.Protocol;
using ImprovWifi.Transport.Linux.Aot;
using var server = new LinuxAotImprovServer(new LinuxAotImprovServerOptions
{
DeviceName = "improv-linux-demo",
AdapterName = "hci0",
AutoAuthorize = false
});
server.IdentifyRequested += (_, _) =>
{
Console.WriteLine("identify requested");
// 演示场景下直接授权,生产环境建议改成用户可感知的确认流程
server.Authorize(true);
};
server.ProvisioningRequested += (_, e) =>
{
Console.WriteLine($"ssid={e.Ssid}, password={e.Password}");
var connected = ConnectToWifi(e.Ssid, e.Password);
if (!connected)
{
server.FailProvisioning();
return;
}
server.CompleteProvisioning("http://192.168.1.10/");
};
server.Start();
Console.WriteLine("Improv service started. Press ENTER to stop.");
Console.ReadLine();
server.Stop();
static bool ConnectToWifi(string ssid, string password)
{
// 这里替换成你自己的联网逻辑,比如调用 nmcli、NetworkManager 或系统 API
return !string.IsNullOrWhiteSpace(ssid);
}
这段代码背后的思路其实很简单:
- 用
LinuxAotImprovServer把Improv服务挂起来 - 在
IdentifyRequested中决定是否授权 - 在
ProvisioningRequested中接收 Wi-Fi 信息并执行实际联网 - 根据联网结果调用
CompleteProvisioning(...)或FailProvisioning(...)
如果你只是想做一个"板端收到 Wi-Fi 信息后立即返回 URL"的最小版本,那么甚至连真实联网逻辑都可以先不接,先把 BLE 配网链路跑通再说。
5.2 什么时候该用 AOT 包
如果你只是本地开发和联调,直接用仓库里的 ImprovWifi.Demo.Linux 通常就够了。
但如果你的目标是部署到板端、做更轻量的 Linux 运行环境,或者你本身就希望往 NativeAOT 方向走,那么 ImprovWifi.Transport.Linux.Aot 会更适合。仓库里对应还有:
ImprovWifi.Demo.Linux.Aotnative/improvwifi_transport_linux_aot_native
其中原生部分是一个 Rust 动态库,负责底层 BlueZ 交互;上层依然保持 .NET 的调用方式。这种拆法的好处,是业务代码不用为了 AOT 场景重写一套接口。
6. 配网时可以用什么客户端
目前这个项目可以配合下面两种方式使用:
- 微信小程序:
Improv 蓝牙配网 - 官方网页工具:improv-wifi.com
如果你是拿手机直接做联调,小程序会更顺手一些,可以微信搜索 "Improv 蓝牙配网";如果你在桌面环境里做调试,官方网页工具通常更方便观察流程。
配网界面示意如下:

7. 什么时候适合用它
如果你现在处在下面这些场景里,这个项目会比较适合:
- 想在
.NET中实现Improv Wi-Fi协议 - 想做跨平台的 BLE 配网服务
- 想先用 Demo 快速验证协议链路
- 想把配网能力接到自己的业务程序中
当然,它也有明确边界。仓库里的 Demo 默认不会直接替你完成系统层面的联网,也不建议把自动授权逻辑原样带到生产环境。生产场景下,授权动作最好由用户显式触发,比如按钮确认、设备物理操作或者其他可感知的授权方式。
8. 总结
这次整理 Sang.ImprovWifi,我想解决的并不是"怎么随手写一个蓝牙配网 Demo",而是"怎么把 Improv Wi-Fi 做成一个结构清晰、可复用、能跨平台继续扩展的 .NET 项目"。
如果你只是想验证流程,可以直接运行仓库里的 Windows 或 Linux Demo;如果你已经有自己的 Linux 项目,那就可以直接试试 ImprovWifi.Transport.Linux.Aot 这个 NuGet 包,把配网能力接进去。
项目地址放在这里:
如果你也正好在做设备配网相关的事情,希望这份项目和这篇文章,能帮你少走一些弯路。