简介:SimpleBTComms 是一份面向 C# 初学者与物联网爱好者的蓝牙通信入门示例,核心演示如何让 C# 程序与 Arduino 等蓝牙设备进行无线数据交换,适合作为课程设计或 DIY 项目的参考起点。资源包共 12 个文件,以 6 个 cs 源码文件为主,辅以 resx 资源、sln 解决方案、csproj 工程配置及说明文档,压缩后约 125KB,体量轻便,便于快速导入 Visual Studio 阅读与调试。项目围绕蓝牙设备发现与枚举、基于服务 UUID 的连接管理、Stream 数据收发、异常捕获与断开连接等环节展开,并提及 BLE 兼容性、跨平台限制与安全风险等注意事项,可帮助读者建立蓝牙通信的整体认知。目前已有 409 人学习下载。由于项目年代较早,代码可能未适配最新蓝牙标准,建议在理解流程后结合 System.Bluetooth.LE 或第三方库进行更新与扩展。
1. 从零手搓一个 C# 蓝牙通讯模块,为什么我建议你先看这个示例
去年帮一个做工业数据采集的团队排查问题,他们的 C# 上位机跟蓝牙称重仪通讯,十次里有三次连不上,连上了也时不时丢包。翻代码发现,蓝牙这块是直接调 32feet 库硬写的,配对、绑定、串口映射、异常重连全揉在一个按钮事件里,没有任何分层。这种场景其实很常见——C# 做蓝牙通讯,难的不是调 API,而是把「发现设备、配对、建立连接、收发数据、断线处理」这条链路理清楚。SimpleBTComms 这个示例项目就是冲着这件事来的:它用 C# 把蓝牙串口通讯的完整流程拆成可复用的模块,代码量不大,但结构清晰,适合拿来做二次开发的起点。如果你正在用 C# 对接蓝牙设备,或者想找一个能跑通的蓝牙通讯骨架,这个项目值得花半小时拆一遍。
2. 项目定位与技术选型:它到底解决了什么问题
2.1 蓝牙串口通讯在 C# 里的真实痛点
蓝牙设备分两大类:BLE(低功耗蓝牙)和经典蓝牙。工业场景里的称重仪、打印机、扫码枪,绝大多数走的是经典蓝牙的 SPP(Serial Port Profile)协议,也就是把蓝牙连接模拟成一个串口。C# 要跟这类设备通讯,绕不开几个坑:一是设备发现和配对是两回事,很多示例代码只做了发现就以为能连;二是配对成功后系统会分配一个虚拟串口,但这个串口号是动态的,不能写死;三是蓝牙连接本身不稳定,断线重连逻辑必须自己兜底。
SimpleBTComms 的定位很明确:它不追求覆盖所有蓝牙协议,而是聚焦经典蓝牙 SPP 这一条链路,把「扫描设备 → 配对 → 获取串口 → 打开串口 → 收发数据 → 关闭释放」做成一个闭环。项目用 C# 编写,基于 .NET 的蓝牙 API 封装,不依赖第三方商业库,适合嵌入到 WinForm 或 WPF 上位机里。
2.2 技术栈与依赖选择
项目主体是 C#,运行在 .NET Framework 或 .NET 环境上。蓝牙通讯部分,常见做法是调用 Windows 的蓝牙 API,或者用 32feet.NET 这类开源库做封装。SimpleBTComms 走的是偏底层的路线,直接跟系统蓝牙栈打交道,好处是依赖少、可控性强,坏处是代码量会多一些,需要自己处理设备枚举和配对状态。
从工程角度看,这个选型是合理的。工业上位机往往部署在离线环境,少一个第三方依赖就少一个部署风险。而且直接调系统 API,遇到问题时排查路径更短,不用去翻库的源码。
提示:如果你的项目需要同时支持 BLE 和经典蓝牙,这个示例的架构需要扩展,它只覆盖了 SPP 这一条路径。
2.3 适合谁用
这个项目适合三类人:一是刚接触 C# 蓝牙开发、想找一个能跑通的参考实现的新手;二是需要快速给上位机加蓝牙通讯功能、不想从零踩坑的开发者;三是想理解蓝牙 SPP 通讯完整链路、为后续做 BLE 或自定义协议打基础的人。如果你要的是开箱即用的商业级蓝牙中间件,这个示例可能不够,但作为学习和二次开发的起点,它的结构是合格的。
3. 目录结构与核心模块拆解:一条数据从发送到接收走了哪些路
3.1 项目文件组织
SimpleBTComms 的代码组织遵循了典型的 C# 项目习惯,核心逻辑集中在几个类文件里。常见的目录结构大致是这样:
SimpleBTComms/ ├── BluetoothManager.cs // 蓝牙设备管理:扫描、配对、状态查询 ├── SerialPortHelper.cs // 串口封装:打开、关闭、读写、事件 ├── DeviceInfo.cs // 设备信息模型:名称、地址、配对状态 ├── MainForm.cs // 界面逻辑:设备列表、连接按钮、数据展示 └── Program.cs // 入口这个划分的好处是职责清晰:BluetoothManager 只管蓝牙层面的设备发现和配对,SerialPortHelper 只管串口层面的数据收发,两者通过设备信息模型传递数据。二次开发时,你要换 UI 或者换通讯协议,改动范围是可控的。
3.2 设备扫描与配对模块
蓝牙通讯的第一步是找到设备并完成配对。很多示例代码在这里偷懒,只做设备发现就以为能连,结果打开串口时报错。SimpleBTComms 把扫描和配对分开处理,逻辑更接近真实场景。
// BluetoothManager.cs public List<DeviceInfo> ScanDevices() { var devices = new List<DeviceInfo>(); // 获取本机蓝牙无线电 var radios = BluetoothRadio.AllRadios; if (radios == null || radios.Length == 0) throw new InvalidOperationException("未检测到蓝牙适配器"); // 发现附近设备,超时时间设为 10 秒 var discovered = radios[0].DiscoverDevices(TimeSpan.FromSeconds(10)); foreach (var dev in discovered) { devices.Add(new DeviceInfo { Name = dev.DeviceName, Address = dev.DeviceAddress, IsPaired = dev.InstalledServices.Length > 0 // 有已安装服务说明配对过 }); } return devices; }这段代码的关键点在于DiscoverDevices的超时参数。设太短会漏设备,设太长界面会卡。10 秒是一个经验值,实际项目中可以根据设备类型调整。另外IsPaired的判断用的是InstalledServices,这是判断设备是否已配对的可靠方式,比单纯看名称或地址要准。
配对操作通常需要单独触发,因为配对过程可能弹出系统确认框,不能静默完成。常见做法是调用BluetoothDeviceInfo的配对方法,然后等待配对结果。
3.3 串口映射与数据收发
配对成功后,系统会给设备分配一个虚拟串口。这个串口号不是固定的,必须动态获取。SimpleBTComms 在配对完成后,通过查询设备的服务来找到对应的串口。
// SerialPortHelper.cs public bool OpenPort(string deviceAddress) { // 根据蓝牙地址找到已配对的设备 var device = BluetoothDeviceInfo.FromAddress(deviceAddress); if (device == null || !device.Authenticated) return false; // 获取设备提供的串口服务 var services = device.InstalledServices; foreach (var service in services) { // SPP 服务的 GUID 是固定的 if (service == BluetoothService.SerialPort) { // 获取该服务对应的虚拟串口号 string portName = device.GetServicePort(service); _serialPort = new SerialPort(portName, 9600, Parity.None, 8, StopBits.One); _serialPort.DataReceived += OnDataReceived; _serialPort.Open(); return true; } } return false; }这里有几个参数需要说明:9600是波特率,工业设备常见的是 9600 或 115200,必须跟设备端一致,否则收到的就是乱码;Parity.None、8、StopBits.One是标准的 8N1 配置,绝大多数设备都用这个。DataReceived事件是串口收数据的入口,注意这个事件在后台线程触发,更新 UI 需要做线程调度。
数据发送就简单了,直接调_serialPort.Write()。但要注意,蓝牙串口的写入是异步的,如果发送频率高,需要自己做队列,否则可能丢数据。
3.4 连接状态管理与异常处理
蓝牙连接不稳定是常态,SimpleBTComms 在状态管理上做了基本的兜底。串口关闭、设备移除、配对丢失这些情况,都会触发异常或事件,需要在代码里捕获并更新界面状态。
// 监听串口错误 _serialPort.ErrorReceived += (sender, e) => { // 记录错误类型,常见的有帧错误、奇偶校验错误 Log($"串口错误: {e.EventType}"); // 触发重连或通知上层 OnConnectionLost?.Invoke(this, EventArgs.Empty); };这段代码的价值在于把底层错误暴露给上层。二次开发时,你可以在这里接入重连逻辑,比如延迟 2 秒后重新打开串口。注意不要在主线程里做重连,否则界面会卡死。
4. 跑起来:从环境准备到第一次收发数据
4.1 环境要求与项目加载
跑这个项目之前,先确认几件事:操作系统是 Windows(蓝牙 API 依赖 Windows 蓝牙栈),机器有蓝牙适配器且驱动正常,开发环境用 Visual Studio 2019 或更高版本。项目本身是 C# 控制台或 WinForm 应用,用 .NET Framework 4.6 以上或 .NET 6+ 都可以,具体看项目文件里的目标框架。
加载项目后,先别急着运行。检查一下引用里有没有缺失的程序集,蓝牙相关的 API 通常在System.Management或专门的蓝牙程序集里。如果编译报错找不到BluetoothRadio这类类型,说明缺少引用,需要手动添加。
4.2 编译与首次运行
编译通过后,直接运行程序。如果是 WinForm 界面,你会看到一个设备列表区域和一个连接按钮。点击扫描,程序会调用蓝牙适配器搜索附近设备,这个过程大概持续 10 秒。搜索完成后,列表里会显示设备名称和地址。
第一次运行时,最常见的现象是列表为空。先别怀疑代码,检查一下系统蓝牙是否打开、设备是否处于可被发现模式。很多蓝牙设备默认不广播,需要手动进入配对模式才会被搜到。
注意:如果程序报「未检测到蓝牙适配器」,先确认设备管理器里蓝牙驱动是否正常,而不是去改代码。
4.3 配对与连接设备
从列表里选中你的目标设备,点击配对。这时候系统可能会弹出确认框,需要手动确认。配对成功后,设备状态会变成已配对。然后点击连接,程序会去查找该设备对应的虚拟串口并打开。
连接成功的标志是串口打开且没有异常。如果打开失败,常见原因有三个:一是设备已经被其他程序占用(比如另一个串口工具还开着),二是配对没有真正完成,三是设备端的 SPP 服务没有启动。排查时按这个顺序来,能省不少时间。
4.4 收发数据验证
连接成功后,就可以测试收发了。如果设备是称重仪这类主动上报数据的,你会看到数据不断刷出来。如果是需要指令触发的,就得先发一条指令。发送数据时,注意波特率和数据格式要跟设备手册一致。
验证收发是否正常,最简单的办法是发一条已知指令,看设备是否有预期响应。如果收到乱码,九成是波特率不对;如果完全没响应,检查指令格式和结束符。工业设备对指令的结束符很敏感,有的要\r\n,有的只要\r,这个必须看手册。
5. 避坑与常见问题:蓝牙通讯里那些让人抓狂的瞬间
5.1 设备搜不到或列表为空
现象是点击扫描后列表一直空着。原因通常有三个:蓝牙适配器没开、设备不在可发现模式、或者扫描超时太短。解决方法是先确认系统蓝牙正常,再让设备进入配对模式,最后把扫描超时从 10 秒调到 15 秒试试。如果还是不行,用系统自带的蓝牙设置搜一下,能搜到说明是代码问题,搜不到就是硬件或驱动问题。
5.2 配对成功但打开串口失败
这是最典型的坑。现象是设备显示已配对,但连接时抛异常说串口不存在。原因是配对和串口映射是两步,配对成功不代表虚拟串口已经创建好。解决方法是配对后等一两秒再获取串口,或者监听系统设备变更事件。另一个常见原因是串口被占用,检查一下有没有其他程序开着这个口。
5.3 数据收发乱码或丢包
乱码基本都是波特率或数据位配置不对。先确认设备手册里的串口参数,常见的是 9600-8-N-1,但也有 115200 或 7 位数据位的。丢包则通常是发送频率太高,串口缓冲区溢出。解决办法是在发送端加一个队列,控制发送间隔,或者用WriteTimeout做超时保护。
5.4 断线后无法重连
蓝牙断线后,直接重新打开串口往往会失败,因为系统还没释放旧的串口句柄。正确的做法是先关闭串口、释放资源,等一两秒再重新走配对和打开流程。如果设备已经不在范围内,还需要重新扫描。这个逻辑在 SimpleBTComms 里只做了基础处理,二次开发时建议封装成一个带重试次数的重连方法。
5.5 界面卡死或无响应
蓝牙扫描和串口打开都是阻塞操作,如果放在 UI 线程里执行,界面就会卡住。解决办法是把这些操作放到后台线程,用Task.Run或BackgroundWorker包起来,完成后通过Invoke更新 UI。这个坑在 WinForm 项目里特别常见,新手很容易踩。
6. 二次开发:把这个示例改成你自己的蓝牙通讯模块
拿到 SimpleBTComms 之后,最直接的改造方向是把它封装成一个可复用的类库,而不是每次新项目都复制粘贴代码。我的做法是先把BluetoothManager和SerialPortHelper抽到一个独立的 C# 类库里,对外只暴露几个方法:ScanDevices()、PairDevice(address)、Connect(address)、Send(data)、Disconnect()。这样 UI 层只负责调用和展示,逻辑层完全解耦。
改造时有一个关键点:事件模型的设计。蓝牙通讯是异步的,设备状态变化、数据到达、连接断开都需要通知上层。建议定义一套自定义事件参数,比如DeviceDiscoveredEventArgs、DataReceivedEventArgs、ConnectionLostEventArgs,让上层订阅。这样你的模块就能同时用在 WinForm、WPF 甚至控制台程序里。
另一个值得做的改造是配置化。把波特率、扫描超时、重连次数这些参数抽到配置文件里,不同设备用不同配置,不用改代码重新编译。下面是一个简单的配置类示例:
// BtConfig.cs public class BtConfig { public int ScanTimeoutSeconds { get; set; } = 10; public int BaudRate { get; set; } = 9600; public int ReconnectRetries { get; set; } = 3; public int ReconnectDelayMs { get; set; } = 2000; }这个类可以用 JSON 或 XML 序列化,运行时加载。参数说明:ScanTimeoutSeconds控制扫描时长,设备多的时候调大;BaudRate必须跟设备一致;ReconnectRetries和ReconnectDelayMs控制断线重连策略,工业场景建议重试 3 次、间隔 2 秒。
验证改造是否成功,我一般会写一个简单的测试程序,模拟设备断线、重连、数据收发,看模块是否能正确处理。特别是断线重连,一定要在真实设备上测,模拟环境测不出蓝牙栈的玄学问题。
从那以后我每次封装蓝牙模块,都会强制走一遍「扫描-配对-连接-收发-断开-重连」的完整流程,不跳过任何一步。很多问题就藏在这些边界情况里,跑通了再交给别人用,心里才有底。希望帮到你。
本文还有配套的精品资源,点击获取