这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 【蓝牙信道探测开发板深度测评与创意应用分享】使用C#实现蓝牙控制开发板LED灯

共1条 1/1 1 跳转至页

【蓝牙信道探测开发板深度测评与创意应用分享】使用C#实现蓝牙控制开发板LED灯

高工
2026-10-30 15:41:00     打赏

一、项目概述

本项目是一个基于 C# 与 .NET 10 的 Windows 桌面调试工具,用于与意法半导体 NUCLEO-WBA65RI 开发板(STM32WBA65)上运行的 BLE_p2pServer 固件进行无线交互。程序同时集成了低功耗蓝牙(BLE)与串口(UART)两条调试通道,采用左右分栏的界面布局,把蓝牙操作、串口收发、分类彩色日志集中在一个窗口内显示,避免在多个工具之间反复切换。

项目定位为研发阶段的调试工具,而不是量产软件。它的核心价值在于:把"扫描设备、建立连接、下发控制指令、观察设备回传、同时抓取串口日志"这一整套流程压缩进一个可以离线分发的单文件可执行程序,方便在方案评审、现场演示或故障排查时快速上手,不依赖任何安装步骤。

程序以 WPF 构建用户界面,以 Plugin.BLE 作为跨平台蓝牙抽象层(在 Windows 平台由 WinRT 后端实现),串口部分使用 System.IO.Ports 直接驱动。最终交付形式为自包含单文件可执行程序,目标机器无需预装 .NET 运行时。

二、基础架构设计

2.1 分层思路

工程刻意保持极简的分层,总共只有三个关注点:界面表现、蓝牙服务封装、项目配置与发布。没有引入依赖注入容器,也没有引入 MVVM 框架,原因是工具型应用规模小、状态少,过度分层反而会拉长每次改动的路径。

表现层

由 MainWindow.xaml 与 MainWindow.xaml.cs 组成。XAML 负责静态布局,包括分栏、分组框、按钮、下拉选择框与日志区域;代码后置文件负责事件绑定、控件状态切换与日志输出。所有控件都通过 x:Name 直接引用,不采用数据绑定,这样可以显著减少样板代码。

服务层

由 BleService.cs 承担。它把 Plugin.BLE 中的适配器、设备、服务、特征值等概念收敛到一个类里,对外只暴露四个事件(设备发现、连接成功、连接断开、按键通知)和若干方法(开始扫描、停止扫描、连接、断开、设置 LED、读取 LED)。界面层不需要了解任何 GATT 细节,只面向这组事件与方法编程。

配置与发布层

由 BLEController.Desktop.csproj 与 dist 目录承担。csproj 声明目标框架、WPF 开关、程序集名称与两个 NuGet 依赖;dist 目录存放发布后的单文件可执行程序,与源码物理隔离,避免出现"改了代码却仍在运行旧程序"的混乱。

2.2 数据流

蓝牙方向的数据流是单向扇出:外设状态变化先进入 Plugin.BLE 的回调,由 BleService 转换成普通的 C# 事件,界面层订阅事件后通过 Dispatcher 切回 UI 线程更新控件。串口方向的数据流相反,由 SerialPort 的后台线程触发 DataReceived,界面层读走字节后同样经 Dispatcher 回到 UI 线程处理。

两条链路共用同一套日志写入方法,但分别写入两个独立的富文本控件,因此在界面上可以同时观察蓝牙与串口两路信息而互不干扰。

三、工程目录结构

在清理掉安卓相关代码之后,仓库只保留桌面工程,目录结构如下。

BLEController/

├── BLEController.sln                 解决方案文件,仅包含桌面项目

└── BLEController.Desktop/

├── dist/EEPW蓝牙信号探测.exe      发布产物,单文件免安装

├── bin/  obj/                     编译输出与中间缓存,可随时删除

├── App.xaml / App.xaml.cs         WPF 应用入口,StartupUri 指向主窗口

├── MainWindow.xaml                界面布局定义

├── MainWindow.xaml.cs             界面逻辑、事件处理与日志输出

├── BleService.cs                  BLE 服务封装

├── BLEController.Desktop.csproj   项目配置

└── README.md                      使用说明

各文件的职责边界很清晰:只要不新增功能,日常改动基本落在 MainWindow.xaml.cs 与 BleService.cs 两个文件里;只有调整目标框架或增删依赖时才需要修改 csproj。

部分代码展示如下所示:

    private void OnDeviceFound(object? sender, ScanResult scan){// 扫描发现不再写入 BLE 日志,设备列表实时显示即可
        Dispatcher.Invoke(() =>
        {
            ScanStatusText.Text = $"扫描中... 已发现 {_ble.ScannedDevices.Count} 台";
        });
    }private void OnScanCompleted(object? sender, int count){
        Dispatcher.Invoke(() =>
        {
            ScanStatusText.Text = $"扫描完成,共 {count} 台";
            ScanBtn.IsEnabled = true;
            StopScanBtn.IsEnabled = false;
        });
    }// ============ BLE 连接 ============private async void ConnectBtn_Click(object sender, RoutedEventArgs e){if (DeviceListBox.SelectedItem is not ScanResult selected){return;
        }try{
            ConnectBtn.IsEnabled = false;

            await _ble.ConnectAsync(selected.Id);
            DeviceInfoText.Text = $"已连接: {_ble.DeviceName}  [{_ble.DeviceId}]";
        }catch (Exception ex){}finally{
            ConnectBtn.IsEnabled = true;
            UpdateBleState(_ble.IsConnected);
        }}private async void DisconnectBtn_Click(object sender, RoutedEventArgs e){try{await _ble.DisconnectAsync();
        }catch{}}private void OnConnectionStateChanged(object? sender, bool connected){
        Dispatcher.Invoke(() =>
        {UpdateBleState(connected);
            if (!connected)
                DeviceInfoText.Text = "已断开";
        });
    }private void UpdateBleState(bool connected){
        Dispatcher.Invoke(() =>
        {
            DisconnectBtn.IsEnabled = connected;
            ConnectBtn.IsEnabled = !connected;
            LedOnBtn.IsEnabled = connected;
            LedOffBtn.IsEnabled = connected;
            LedReadBtn.IsEnabled = connected;
            SwitchStartBtn.IsEnabled = connected;
            SwitchStopBtn.IsEnabled = connected;

            StatusBarBle.Text = connected ? $"BLE: 已连接 {_ble.DeviceName}" : "BLE: 未连接";
            StatusBarBle.Foreground = connected ? Brushes.DarkGreen : Brushes.Gray;
        });
    }// ============ LED 控制 ============private async void LedOnBtn_Click(object sender, RoutedEventArgs e){try{AppendBleLog($"TX → FE41 开灯 payload: {{0x01, 0x01}}", ColorTx);
            await _ble.WriteLedAsync(on: true);
            AppendBleLog("✓ LED 已点亮", ColorInfo);
        }catch (Exception ex){AppendBleLog($"开灯失败: {ex.Message}", ColorError);
        }}private async void LedOffBtn_Click(object sender, RoutedEventArgs e){try{AppendBleLog($"TX → FE41 关灯 payload: {{0x01, 0x00}}", ColorTx);
            await _ble.WriteLedAsync(on: false);
            AppendBleLog("✓ LED 已熄灭", ColorInfo);
        }catch (Exception ex){AppendBleLog($"关灯失败: {ex.Message}", ColorError);
        }}private async void LedReadBtn_Click(object sender, RoutedEventArgs e){try{AppendBleLog("TX → FE41 回读 LED 状态", ColorTx);
            var (data, _) = await _ble.ReadLedAsync();
            AppendBleLog($"RX ← FE41 回读: {ToHex(data)}  ASCII: {ToAscii(data)}", ColorRx);
        }catch (Exception ex){AppendBleLog($"回读失败: {ex.Message}", ColorError);
        }}

四、运行的基本需求

4.1 终端用户侧

操作系统要求 Windows 10 版本 1809(内部版本 17763)及以上,或 Windows 11。之所以存在这个下限,是因为 Plugin.BLE 的 Windows 实现依赖 WinRT 的蓝牙通用 API,而这套 API 从 1809 开始才完整可用。

硬件方面,电脑必须具备蓝牙适配器,且需要支持 BLE 4.0 及以上。绝大多数笔记本内置的无线网卡都满足要求,早年台式机外接的经典蓝牙 2.1 适配器则无法满足,需要更换为支持低功耗蓝牙的模块。

若使用串口功能,需要 USB 转 TTL 模块(CH340、CP210x、FTDI 等常见芯片均可)以及对应驱动,并确保模块的 TX 与开发板 RX 交叉连接、两者共地。

由于采用自包含单文件发布,终端用户不需要安装 .NET 运行时,也不需要安装任何第三方运行库,直接双击即可运行。

4.2 开发调试侧

需要 .NET 10 SDK。本机实际使用 Visual Studio 2026 自带的 .NET 11 RC SDK 进行编译,能够正常编译 net10.0-windows 目标,但不建议把项目目标框架直接改成 net11,因为那会拉高运行环境的门槛。

需要 WPF 相关组件,安装 Visual Studio 时勾选".NET 桌面开发"工作负载即可获得。若需要重新出包,还需要能够访问 NuGet 的网络环境。

如果需要重新编译安卓版本(当前仓库已删除安卓源码),则额外需要 Android SDK Platform、Build-Tools、Platform-Tools 以及 OpenJDK。这部分依赖只影响移动端,与桌面工程无关。

五、已实现的功能

04-1 C#上位机截图.png

5.1 蓝牙通道

扫描附近 BLE 设备,只把带有广播名称的设备加入列表,并支持随时停止扫描,避免无意义的持续扫描消耗适配器带宽。选中设备后可发起连接,连接过程会自动先停止扫描,防止扫描与连接争用同一块蓝牙控制器。

连接成功后自动按 UUID 定位 P2P Server 服务,取出 LED 控制特征值与按键通知特征值。若服务缺失会明确抛出"未找到 P2P Server 服务"的提示,而不是静默失败,便于快速判断固件版本是否正确。

提供 LED 开、LED 关、读取当前状态三个操作按钮。写入采用无响应写入方式,与固件声明的特征值属性保持一致;写入之后程序会延迟约一百五十毫秒再回读一次,用于确认指令确实落到了固件里。这一步是排查"能开不能关"类问题的关键手段。

订阅按键通知特征值,实时显示开发板板载按键的"按下"与"释放"状态,同时在日志区打印原始字节,方便核对负载格式。

5.2 串口通道

串口列表自动枚举,支持手动刷新;波特率、数据位、校验位、停止位四个参数均可在界面上设置,波特率覆盖 9600 到 921600 的常用档位。串口打开期间参数控件会被禁用,防止运行期修改造成界面配置与实际参数不一致。

接收区支持两种显示模式:勾选 HEX 时按十六进制字节串显示,不勾选时按 UTF-8 文本显示;配合"自动滚动"开关可以决定接收区是否跟随最新数据。

5.3 日志与界面

界面采用左右分栏,左侧放置蓝牙操作,右侧放置串口操作,中间由可拖动分隔条划分。底部再分两栏,左边是蓝牙日志,右边是串口日志。

日志按语义着色:红色表示本机发出的写入或发送,蓝色表示从设备收到的通知或数据,黑色为普通流程信息,橙色为警告,深红色为错误。这样即使同时操作两路,也能一眼分辨数据的流向。

六、依赖包与工具链

项目直接依赖的 NuGet 包只有两个。其一是 Plugin.BLE,版本 3.1.0,提供跨平台蓝牙抽象,在 Windows 下由 WinRT 后端实现,是整个蓝牙功能的地基。其二是 System.IO.Ports,版本 9.0.0,提供串口能力;从 .NET Core 起串口不再是框架内置组件,必须显式引用。

框架层面依赖 Microsoft.NETCore.App 与 Microsoft.WindowsDesktop.App,后者包含 WPF。执行自包含发布时,这两个共享框架会连同运行库一起被打进可执行文件,因此最终体积较大。

构建工具链包括 .NET SDK、MSBuild、NuGet 客户端,以及单文件打包所需的宿主组件。发布阶段还会用到 singlefilehost 与压缩逻辑,这些都由 SDK 自动处理,无需单独安装。

调试阶段建议配合 Visual Studio 2026 使用,它的 WPF 可视化调试器、实时可视化树与 XAML 热重载功能,对界面调整效率提升明显。

七、编译与发布流程

日常调试只需在 Visual Studio 中按 F5,或使用命令行构建。正式交付则需要发布为自包含单文件。常用命令如下。

dotnet build BLEController.Desktop.csproj -c Releasedotnet publish BLEController.Desktop.csproj -c Release -r win-x64 ^--self-contained true -p:PublishSingleFile=true ^-p:IncludeNativeLibrariesForSelfExtract=true ^-p:EnableCompressionInSingleFile=true

第一条命令生成框架依赖版本,体积小,但要求目标机器已安装 .NET 10 运行时;第二条生成自包含单文件,体积约六十八兆,可在任意六十四位 Windows 10 或 Windows 11 上直接运行。

发布完成后,需要把 publish 目录中的可执行文件复制到 dist 目录覆盖旧文件,这是当前约定的交付动作。每次改完代码务必确认 dist 中文件的时间戳已经更新,否则很容易测试到旧程序却误判为新问题。

八、与 STM32 固件的协议对接

固件位于同一工程目录下的 STM32_WPAN/App 中。服务定义为自定义一百二十八位 UUID:服务标识为 FE40,LED 控制特性为 FE41,按键通知特性为 FE42。LED 特性同时具备读与无响应写两种属性,按键特性只做通知。

LED 指令的正确负载是两个字节,第一个字节表示设备选择(固定为 0x01),第二个字节表示目标状态(0x01 为开,0x00 为关)。固件判断的正是第二个字节,因此只发送一个字节会导致状态判断依赖上一次的残留值,在现象上表现为"能开却不能关"。

按键通知的负载同样是两个字节,程序读取第二个字节来判断按下与释放,第一个字节仅作为设备选择位。

九、开发注意事项

04-2 C#上位机截图代码截图.png

9.1 WPF 线程模型

串口的数据接收回调运行在后台线程,而 WPF 控件具有线程亲和性,只能在创建它的 UI 线程上访问。任何在 DataReceived 处理函数中直接读写控件的写法,都会抛出"调用线程无法访问此对象"的异常。正确做法是把整段界面更新逻辑包进 Dispatcher.Invoke,并且把 CheckBox 这类控件的状态读取放进 Invoke 内部,避免在后台线程求值。

9.2 富文本着色

WPF 的 RichTextBox 与 WinForms 的同名控件 API 完全不同,前者没有 SelectionBrush 与 SelectionColor 这类属性,写了也不会报错,但完全不生效。给追加文本着色的正确方式是构造一个 TextRange,先赋值文本内容,再调用 ApplyPropertyValue 设置 Foreground 属性。

9.3 单文件发布的注意事项

WPF 依赖若干原生 DLL,包括图形渲染与文本输入相关的组件。若发布时漏掉 IncludeNativeLibrariesForSelfExtract 参数,这些 DLL 会留在可执行文件旁边而不会被打进包体,此时只拷贝单个 exe 出去会导致程序启动即闪退,且没有任何错误提示。判断方法是观察 publish 目录是否只剩下 exe 与 pdb,若还有若干 dll 就说明没有打包干净。

9.4 蓝牙写入的静默丢失

无响应写入不会返回确认,连接状态不佳时数据可能丢失而不报错。因此关键指令写入后应主动回读校验,这也是本工程在设置 LED 之后追加一次读取操作的原因。

9.5 串口资源管理

串口是独占资源,被其它程序占用时打开会直接失败。刷新端口列表之前必须先关闭当前串口。程序退出时需要解绑 DataReceived 事件并释放 SerialPort 对象,否则句柄泄漏会导致下次打开同名端口失败。

9.6 命名与本地化

程序集名称使用中文"EEPW蓝牙信号探测",这在 .NET 上完全可行,按钮与窗口标题也直接使用中文。需要注意的是,命令行中复制中文文件名要加引号,部分老旧打包脚本对非 ASCII 文件名支持不佳。

9.7 目标框架的选择

目标框架必须写成 net10.0-windows10.0.19041.0 这种带 Windows 版本号的形式,否则 WinRT 投影不可用,Plugin.BLE 将无法通过编译。适当降低 Windows 版本号可以让更多机器可运行,但蓝牙 API 的完整性会有损失,需要在兼容性与功能之间权衡。

十、常见问题与排查思路

04-3 C#上位机截图调试截图.png

C#调试截图如上所示。

扫描不到任何设备时,先确认系统蓝牙开关已打开、适配器支持低功耗蓝牙,再确认没有其它程序独占蓝牙适配器。

连接后立即断开,多半是设备已被手机端占用。BLE 外设通常只接受一个中心设备连接,需要先在手机上断开。

提示未找到 FE40 服务,说明板子上运行的不是 BLE_p2pServer 固件,或者固件尚未完成服务注册。建议先用手机端官方工具交叉验证。

串口打开失败并提示端口被占用,应检查是否有 STM32CubeProgrammer、串口助手一类程序仍持有该端口。

串口可以打开但收不到数据,按顺序核对波特率与帧格式是否与固件初始化参数一致、TX 与 RX 是否交叉连接、两者是否共地。

程序启动后立即退出且没有任何提示,应首先怀疑单文件打包不完整,回到 publish 目录检查是否有遗漏的原生 DLL。

十一、后续可扩展方向

在协议层,可以把 UUID 与负载格式抽取为常量表,并支持通过配置文件覆盖,方便适配不同的固件变体。在功能层,可以增加 OTA 固件升级、广播包解析、RSSI 实时曲线,以及串口发送与十六进制输入。在工程层,可以把日志改为可落盘的环形缓冲,或引入轻量 MVVM 框架以支撑更复杂的界面。

若未来需要面向量产交付,还应补充异常重连策略、单元测试、安装包签名与自动更新机制。当前版本更偏向研发阶段的调试工具定位,功能取舍均围绕这一目标展开。




关键词: 蓝牙     信道     探测     C#上位机软件    

共1条 1/1 1 跳转至页

回复

匿名不能发帖!请先 [ 登陆 注册 ]