一、开发前景
最近一直想实现一款在电脑上就能用的OTA的蓝牙升级程序,网上的都是用串口去实现,太不方便了,经过一段时间的开发,终于能够直接调用Windows电脑自带的蓝牙接口去实现,开发用具用的VS2019,.Net Framework4.8。
我采用的是Ymodem发送方式,UI做得很简陋,只实现了功能,另外受蓝牙MTU设置和波特率等参数配置的限制,发送速度较慢,只有128字节,需要自行摸索如何改良传输速度
二、效果展示


三、代码展示
接下来开始展示代码吧:
1、Form1.Designer.cs
namespace TCM_OTA_APP
{
partial class Form1
{
/// <summary>
/// 必需的设计器变量。
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 清理所有正在使用的资源。
/// </summary>
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows 窗体设计器生成的代码
/// <summary>
/// 设计器支持所需的方法 - 不要修改
/// 使用代码编辑器修改此方法的内容。
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.btnSearch = new System.Windows.Forms.Button();
this.btnConnect = new System.Windows.Forms.Button();
this.listboxBleDevice = new System.Windows.Forms.ListBox();
this.tableLayoutPanelTop = new System.Windows.Forms.TableLayoutPanel();
this.btnOpteron = new System.Windows.Forms.Button();
this.btnLoader = new System.Windows.Forms.Button();
this.btnSend = new System.Windows.Forms.Button();
this.linkLabelEdit1 = new ReaLTaiizor.Controls.LinkLabelEdit();
this.richTextBox1 = new System.Windows.Forms.RichTextBox();
this.tableLayoutPanelTop.SuspendLayout();
this.SuspendLayout();
//
// btnSearch
//
this.btnSearch.Dock = System.Windows.Forms.DockStyle.Fill;
this.btnSearch.Location = new System.Drawing.Point(3, 4);
this.btnSearch.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.btnSearch.Name = "btnSearch";
this.btnSearch.Size = new System.Drawing.Size(113, 58);
this.btnSearch.TabIndex = 0;
this.btnSearch.Text = "搜索";
this.btnSearch.UseVisualStyleBackColor = true;
this.btnSearch.Click += new System.EventHandler(this.btnSearch_Click);
//
// btnConnect
//
this.btnConnect.Dock = System.Windows.Forms.DockStyle.Fill;
this.btnConnect.Location = new System.Drawing.Point(122, 4);
this.btnConnect.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.btnConnect.Name = "btnConnect";
this.btnConnect.Size = new System.Drawing.Size(122, 58);
this.btnConnect.TabIndex = 1;
this.btnConnect.Text = "匹配";
this.btnConnect.UseVisualStyleBackColor = true;
this.btnConnect.Click += new System.EventHandler(this.btnConnect_Click);
//
// listboxBleDevice
//
this.listboxBleDevice.Dock = System.Windows.Forms.DockStyle.Top;
this.listboxBleDevice.FormattingEnabled = true;
this.listboxBleDevice.ItemHeight = 18;
this.listboxBleDevice.Location = new System.Drawing.Point(0, 199);
this.listboxBleDevice.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.listboxBleDevice.Name = "listboxBleDevice";
this.listboxBleDevice.Size = new System.Drawing.Size(1473, 220);
this.listboxBleDevice.TabIndex = 1;
//
// tableLayoutPanelTop
//
this.tableLayoutPanelTop.ColumnCount = 4;
this.tableLayoutPanelTop.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 119F));
this.tableLayoutPanelTop.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tableLayoutPanelTop.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 118F));
this.tableLayoutPanelTop.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 1108F));
this.tableLayoutPanelTop.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20F));
this.tableLayoutPanelTop.Controls.Add(this.btnOpteron, 0, 3);
this.tableLayoutPanelTop.Controls.Add(this.btnLoader, 0, 3);
this.tableLayoutPanelTop.Controls.Add(this.btnSend, 0, 3);
this.tableLayoutPanelTop.Controls.Add(this.btnSearch, 0, 0);
this.tableLayoutPanelTop.Controls.Add(this.btnConnect, 1, 0);
this.tableLayoutPanelTop.Controls.Add(this.linkLabelEdit1, 3, 3);
this.tableLayoutPanelTop.Dock = System.Windows.Forms.DockStyle.Top;
this.tableLayoutPanelTop.Location = new System.Drawing.Point(0, 0);
this.tableLayoutPanelTop.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.tableLayoutPanelTop.Name = "tableLayoutPanelTop";
this.tableLayoutPanelTop.RowCount = 4;
this.tableLayoutPanelTop.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 66F));
this.tableLayoutPanelTop.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 32F));
this.tableLayoutPanelTop.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 29F));
this.tableLayoutPanelTop.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 47F));
this.tableLayoutPanelTop.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F));
this.tableLayoutPanelTop.Size = new System.Drawing.Size(1473, 199);
this.tableLayoutPanelTop.TabIndex = 0;
//
// btnOpteron
//
this.btnOpteron.Dock = System.Windows.Forms.DockStyle.Fill;
this.btnOpteron.Location = new System.Drawing.Point(122, 131);
this.btnOpteron.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.btnOpteron.Name = "btnOpteron";
this.btnOpteron.Size = new System.Drawing.Size(122, 64);
this.btnOpteron.TabIndex = 16;
this.btnOpteron.Text = "连接蓝牙";
this.btnOpteron.UseVisualStyleBackColor = true;
this.btnOpteron.Click += new System.EventHandler(this.btnOpteron_Click);
//
// btnLoader
//
this.btnLoader.Dock = System.Windows.Forms.DockStyle.Fill;
this.btnLoader.Location = new System.Drawing.Point(250, 131);
this.btnLoader.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.btnLoader.Name = "btnLoader";
this.btnLoader.Size = new System.Drawing.Size(112, 64);
this.btnLoader.TabIndex = 15;
this.btnLoader.Text = "选取文件";
this.btnLoader.UseVisualStyleBackColor = true;
this.btnLoader.Click += new System.EventHandler(this.btnLoader_Click);
//
// btnSend
//
this.btnSend.Dock = System.Windows.Forms.DockStyle.Fill;
this.btnSend.Location = new System.Drawing.Point(3, 131);
this.btnSend.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.btnSend.Name = "btnSend";
this.btnSend.Size = new System.Drawing.Size(113, 64);
this.btnSend.TabIndex = 13;
this.btnSend.Text = "发送";
this.btnSend.UseVisualStyleBackColor = true;
this.btnSend.Click += new System.EventHandler(this.btnSend_Click);
//
// linkLabelEdit1
//
this.linkLabelEdit1.ActiveLinkColor = System.Drawing.Color.FromArgb(((int)(((byte)(153)))), ((int)(((byte)(34)))), ((int)(((byte)(34)))));
this.linkLabelEdit1.BackColor = System.Drawing.Color.Transparent;
this.linkLabelEdit1.Font = new System.Drawing.Font("Microsoft Sans Serif", 9F);
this.linkLabelEdit1.LinkBehavior = System.Windows.Forms.LinkBehavior.NeverUnderline;
this.linkLabelEdit1.LinkColor = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(34)))), ((int)(((byte)(37)))));
this.linkLabelEdit1.Location = new System.Drawing.Point(368, 127);
this.linkLabelEdit1.Name = "linkLabelEdit1";
this.linkLabelEdit1.Size = new System.Drawing.Size(896, 38);
this.linkLabelEdit1.TabIndex = 17;
this.linkLabelEdit1.TabStop = true;
this.linkLabelEdit1.Text = "请选择bin文件";
this.linkLabelEdit1.TextAlign = System.Drawing.ContentAlignment.BottomLeft;
this.linkLabelEdit1.VisitedLinkColor = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(34)))), ((int)(((byte)(37)))));
//
// richTextBox1
//
this.richTextBox1.Location = new System.Drawing.Point(3, 433);
this.richTextBox1.Name = "richTextBox1";
this.richTextBox1.ReadOnly = true;
this.richTextBox1.Size = new System.Drawing.Size(1470, 400);
this.richTextBox1.TabIndex = 2;
this.richTextBox1.Text = "";
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 18F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1473, 830);
this.Controls.Add(this.richTextBox1);
this.Controls.Add(this.listboxBleDevice);
this.Controls.Add(this.tableLayoutPanelTop);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.Name = "Form1";
this.Text = "OTA升级";
this.tableLayoutPanelTop.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
// 组件声明
private System.Windows.Forms.Button btnSearch;
private System.Windows.Forms.Button btnConnect;
private System.Windows.Forms.ListBox listboxBleDevice;
private System.Windows.Forms.TableLayoutPanel tableLayoutPanelTop;
private System.Windows.Forms.RichTextBox richTextBox1;
private System.Windows.Forms.Button btnSend;
private ReaLTaiizor.Controls.LinkLabelEdit linkLabelEdit1;
private System.Windows.Forms.Button btnOpteron;
private System.Windows.Forms.Button btnLoader;
}
}
2、Form.cs
一定要改的地方:
(1)这个XXXX是蓝牙名称,我嫌搜到的蓝牙太多了,加了一层过滤,不影响原先的搜索速度
if (!string.IsNullOrEmpty(device.Name) && device.Name.Contains("XXXX"))
(2)// 固定 UUID(来自 蓝牙的 规格书),三个UUID,搜索一下蓝牙能够看得到的
private readonly Guid SERVICE_UUID = new Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
private readonly Guid NOTIFY_UUID = new Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
private readonly Guid WRITE_UUID = new Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Windows.Devices.Bluetooth;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
namespace TCM_OTA_APP
{
public partial class Form1 : Form
{
private bool isSearching = false;
//private bool isReading = false;
private List<Windows.Devices.Bluetooth.BluetoothLEDevice> foundDevices = new List<Windows.Devices.Bluetooth.BluetoothLEDevice>();
private Dictionary<string, Windows.Devices.Bluetooth.BluetoothLEDevice> deviceDictionary = new Dictionary<string, Windows.Devices.Bluetooth.BluetoothLEDevice>();
private YmodemSender _ymodemSender;
private string _lastConnectedMac = null;
private string _selectedFilePath = null;
// 固定 UUID(来自 蓝牙的 规格书)
private readonly Guid SERVICE_UUID = new Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
private readonly Guid NOTIFY_UUID = new Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
private readonly Guid WRITE_UUID = new Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
public Form1()
{
InitializeComponent();
CheckForIllegalCrossThreadCalls = false;
this.bleCore.MessAgeChanged += BleCore_MessAgeChanged;
this.bleCore.DeviceWatcherChanged += BleCore_DeviceWatcherChanged;
this.bleCore.GattDeviceServiceAdded += BleCore_GattDeviceServiceAdded;
this.bleCore.CharacteristicAdded += BleCore_CharacteristicAdded;
_ymodemSender = new YmodemSender(bleCore);
_ymodemSender.ProgressChanged += Ymodem_ProgressChanged;
_ymodemSender.LogMessage += Ymodem_LogMessage;
_ymodemSender.ErrorOccurred += Ymodem_ErrorOccurred;
this.Init();
}
// 异步线程
public static void RunAsync(Action action)
{
((Action)(delegate ()
{
action.Invoke();
})).BeginInvoke(null, null);
}
BleCore bleCore = new BleCore();
/// <summary>
/// 存储检测到的设备
/// </summary>
List<Windows.Devices.Bluetooth.BluetoothLEDevice> DeviceList = new List<Windows.Devices.Bluetooth.BluetoothLEDevice>();
/// <summary>
/// 当前蓝牙服务列表
/// </summary>
List<Windows.Devices.Bluetooth.GenericAttributeProfile.GattDeviceService> GattDeviceServices = new List<Windows.Devices.Bluetooth.GenericAttributeProfile.GattDeviceService>();
/// <summary>
/// 当前蓝牙服务特征列表
/// </summary>
List<Windows.Devices.Bluetooth.GenericAttributeProfile.GattCharacteristic> GattCharacteristics = new List<Windows.Devices.Bluetooth.GenericAttributeProfile.GattCharacteristic>();
private void AppendLog(string text)
{
if (richTextBox1.InvokeRequired)
{
richTextBox1.BeginInvoke(new Action(() => AppendLog(text)));
return;
}
richTextBox1.AppendText(text + Environment.NewLine);
richTextBox1.SelectionStart = richTextBox1.Text.Length;
richTextBox1.ScrollToCaret();
}
private void AppendTextWithNewLine(string text) => AppendLog(text);
private string GetDeviceMacAddress(Windows.Devices.Bluetooth.BluetoothLEDevice device)
{
try
{
byte[] bytes = BitConverter.GetBytes(device.BluetoothAddress);
Array.Reverse(bytes);
return BitConverter.ToString(bytes, 2, 6).Replace('-', ':').ToLower();
}
catch
{
return "Unknown";
}
}
private void btnSearch_Click(object sender, EventArgs e)
{
if (this.btnSearch.Text == "搜索")
{
this.listboxBleDevice.Items.Clear();
this.richTextBox1.Clear();
this.DeviceList.Clear();
this.deviceDictionary.Clear();
foundDevices.Clear();
isSearching = true;
this.bleCore.StartBleDeviceWatcher();
this.btnSearch.Text = "停止";
}
else
{
isSearching = false;
this.bleCore.StopBleDeviceWatcher();
this.btnSearch.Text = "搜索";
// 搜索停止后,一次性显示所有匹配的设备
foreach (var device in foundDevices)
{
//XXXX 写你要升级的蓝牙名字,这样可以排除掉搜到的其他蓝牙
if (!string.IsNullOrEmpty(device.Name) && device.Name.Contains("XXXX"))
{
string macAddress = GetDeviceMacAddress(device);
string displayName = $"{device.Name} (MAC: {macAddress})";
if (!deviceDictionary.ContainsKey(macAddress))
{
this.listboxBleDevice.Items.Add(displayName);
deviceDictionary[macAddress] = device;
this.DeviceList.Add(device);
this.btnConnect.Enabled = true;
// 👇 添加调试信息
this.AppendTextWithNewLine($"添加设备: {displayName}, MAC: {macAddress}");
}
}
}
this.AppendTextWithNewLine($"搜索完成,发现 {deviceDictionary.Count} 个目标设备");
}
}
private async void btnConnect_Click(object sender, EventArgs e)
{
if (null == this.listboxBleDevice.SelectedItem)
{
MessageBox.Show("请先选择一个蓝牙设备", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
string selectedDisplayName = this.listboxBleDevice.SelectedItem.ToString();
this.AppendTextWithNewLine($"选中的设备: {selectedDisplayName}");
// 提取 MAC 地址
string macAddress = null;
System.Text.RegularExpressions.Regex regex = new System.Text.RegularExpressions.Regex(@"([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})");
var match = regex.Match(selectedDisplayName);
if (match.Success)
{
macAddress = match.Value.ToLower();
this.AppendTextWithNewLine($"提取的 MAC 地址: {macAddress}");
}
else
{
this.AppendTextWithNewLine("未能提取到 MAC 地址");
}
Windows.Devices.Bluetooth.BluetoothLEDevice bluetoothLEDevice = null;
if (!string.IsNullOrEmpty(macAddress))
{
if (deviceDictionary.TryGetValue(macAddress, out bluetoothLEDevice))
{
this.AppendTextWithNewLine($"通过 MAC 地址找到设备: {macAddress}");
}
else
{
this.AppendTextWithNewLine($"字典中未找到 MAC 地址: {macAddress}");
foreach (var key in deviceDictionary.Keys)
{
this.AppendTextWithNewLine($"字典中的 MAC: {key}");
}
}
}
else
{
this.AppendTextWithNewLine("未能提取到 MAC 地址");
}
if (bluetoothLEDevice == null)
{
foreach (var device in DeviceList)
{
string deviceMac = GetDeviceMacAddress(device);
if (deviceMac == macAddress)
{
bluetoothLEDevice = device;
break;
}
}
}
if (bluetoothLEDevice == null)
{
MessageBox.Show($"未找到设备: {selectedDisplayName}", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
this.btnConnect.Enabled = false;
this.Cursor = Cursors.WaitCursor;
this.AppendTextWithNewLine($"正在匹配: {selectedDisplayName}");
bleCore.StartMatching(bluetoothLEDevice);
if (bleCore.CurrentDevice != null)
{
_lastConnectedMac = GetDeviceMacAddress(bluetoothLEDevice);
// 自动获取服务和特征
await AutoConfigureDevice();
MessageBox.Show("设备已连接并配置完成", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
catch (Exception ex)
{
MessageBox.Show($"连接异常: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
this.btnConnect.Enabled = true;
this.Cursor = Cursors.Default;
}
}
/// <summary>
/// 自动配置设备(使用固定 UUID)
/// </summary>
private async Task AutoConfigureDevice()
{
AppendTextWithNewLine("开始自动配置设备...");
if (bleCore.CurrentDevice == null)
throw new Exception("设备未连接");
// 1. 获取服务
var servicesResult = await bleCore.CurrentDevice.GetGattServicesAsync(Windows.Devices.Bluetooth.BluetoothCacheMode.Uncached);
if (servicesResult.Status != GattCommunicationStatus.Success)
throw new Exception($"获取服务失败: {servicesResult.Status}");
var targetService = servicesResult.Services.FirstOrDefault(s => s.Uuid == SERVICE_UUID);
if (targetService == null)
throw new Exception($"未找到服务: {SERVICE_UUID}");
AppendTextWithNewLine($"✅ 找到服务: {SERVICE_UUID}");
// ==== 新增:赋值并打印 MTU ====
bleCore.CurrentService = targetService;
int mtuPayload = bleCore.GetMaxPacketSize();
AppendTextWithNewLine($"📊 当前 ATT MTU 有效载荷: {mtuPayload} 字节");
// 2. 获取特征
var characteristicsResult = await targetService.GetCharacteristicsAsync(Windows.Devices.Bluetooth.BluetoothCacheMode.Uncached);
if (characteristicsResult.Status != GattCommunicationStatus.Success)
throw new Exception($"获取特征失败: {characteristicsResult.Status}");
var characteristics = characteristicsResult.Characteristics;
// 3. 配置 Notify 特征 (FFF1) —— 使用 SetOpteron 注册并启用通知
var notifyChar = characteristics.FirstOrDefault(c => c.Uuid == NOTIFY_UUID);
if (notifyChar != null)
{
await bleCore.SetOpteron(notifyChar); // 内部注册事件并启用通知
this.btnLoader.Enabled = true;
AppendTextWithNewLine($"✅ 已配置 Notify 特征: {NOTIFY_UUID}");
}
else
{
AppendTextWithNewLine($"⚠️ 未找到 Notify 特征: {NOTIFY_UUID}");
}
// 4. 配置 Write 特征 (FFF2) —— 直接赋值,无需连接
var writeChar = characteristics.FirstOrDefault(c => c.Uuid == WRITE_UUID);
if (writeChar != null)
{
bleCore.CurrentWriteCharacteristic = writeChar;
this.btnSend.Enabled = true;
AppendTextWithNewLine($"✅ 已配置 Write 特征: {WRITE_UUID}");
}
else
{
AppendTextWithNewLine($"⚠️ 未找到 Write 特征: {WRITE_UUID}");
}
// 5. 更新 UI
this.btnLoader.Enabled = true;
this.btnOpteron.Text = "断开蓝牙";
this.btnOpteron.Enabled = true;
AppendTextWithNewLine("✅ 设备自动配置完成");
}
private async void btnOpteron_Click(object sender, EventArgs e)
{
if (this.btnOpteron.Text == "断开蓝牙")
{
_lastConnectedMac = bleCore.CurrentDeviceMAC?.ToLower();
this.bleCore.Dispose();
this.btnOpteron.Text = "连接蓝牙";
this.btnSend.Enabled = false;
this.btnLoader.Enabled = false;
this.btnLoader.Enabled = false;
this.AppendTextWithNewLine("已断开蓝牙连接");
return;
}
// 如果是"连接蓝牙"状态,尝试重连
if (!string.IsNullOrEmpty(_lastConnectedMac))
{
AppendTextWithNewLine($"尝试重新连接 {_lastConnectedMac} ...");
var device = await bleCore.SelectDeviceFromIdAsync(_lastConnectedMac);
if (device != null)
{
await AutoConfigureDevice();
AppendTextWithNewLine("重连并配置成功");
}
else
{
MessageBox.Show("无法重新连接设备,请重新搜索并连接", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
else
{
MessageBox.Show("请先通过【连接】按钮连接设备", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
private void btnLoader_Click(object sender, EventArgs e)
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.Title = "选择要发送的固件文件";
ofd.Filter = "BIN文件 (*.bin)|*.bin|所有文件 (*.*)|*.*";
ofd.FilterIndex = 1;
ofd.DefaultExt = "bin";
if (ofd.ShowDialog() == DialogResult.OK)
{
_selectedFilePath = ofd.FileName;
// 显示选中文件
this.linkLabelEdit1.Text = _selectedFilePath; // 如果有文本框显示路径
this.richTextBox1.AppendText($"已选择文件: {_selectedFilePath}\n");
// 启用发送按钮
this.btnSend.Enabled = true;
}
}
private async void btnSend_Click(object sender, EventArgs e)
{
// 1. 检查是否已选择有效文件
if (string.IsNullOrEmpty(_selectedFilePath) || !System.IO.File.Exists(_selectedFilePath))
{
MessageBox.Show("请先通过【选择文件】按钮选择固件文件", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 2. 检查蓝牙连接和写特征是否就绪
if (bleCore.CurrentDevice == null || bleCore.CurrentWriteCharacteristic == null)
{
MessageBox.Show("蓝牙未连接或未设置写特征,请先连接并配置特征", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 3. 禁用发送按钮,防止重复点击
btnSend.Enabled = false;
try
{
// 4. 启动 Ymodem 发送(异步)
await _ymodemSender.SendFileAsync(_selectedFilePath);
AppendTextWithNewLine("✅ 文件发送完成");
}
catch (Exception ex)
{
AppendTextWithNewLine($"❌ 发送失败: {ex.Message}");
MessageBox.Show($"发送失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
btnSend.Enabled = true;
}
}
/// <summary>
/// 提示消息
/// </summary>
private void BleCore_MessAgeChanged(MsgType type, string message, byte[] data)
{
RunAsync(() =>
{
if (type == MsgType.BleData)
{
//if (isReading)
//{
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
if (data != null && data.Length > 0)
{
//string hexStr = BitConverter.ToString(data);
//this.AppendTextWithNewLine($"[{timestamp}] 收到 {data.Length} 字节: {hexStr}");
}
else
{
this.AppendTextWithNewLine($"[{timestamp}] {message}");
}
//}
}
else
{
this.AppendTextWithNewLine(message);
}
});
}
/// <summary>
/// 搜索蓝牙设备列表
/// </summary>
private void BleCore_DeviceWatcherChanged(MsgType type, Windows.Devices.Bluetooth.BluetoothLEDevice bluetoothLEDevice)
{
/* RunAsync(() =>
{
this.listboxBleDevice.AppendText(bluetoothLEDevice.Name);
this.DeviceList.Add(bluetoothLEDevice);
this.btnConnect.Enabled = true;
});*/
RunAsync(() =>
{
// 只收集设备,不显示
if (bluetoothLEDevice != null && isSearching)
{
foundDevices.Add(bluetoothLEDevice);
}
});
}
/// <summary>
/// 获取蓝牙服务列表
/// </summary>
private void BleCore_GattDeviceServiceAdded(Windows.Devices.Bluetooth.GenericAttributeProfile.GattDeviceService gattDeviceService)
{
RunAsync(() =>
{
//this.cmbServer.Items.Add(gattDeviceService.Uuid.ToString());
this.GattDeviceServices.Add(gattDeviceService);
// this.btnFeatures.Enabled = true;
});
}
/// <summary>
/// 获取特征列表
/// </summary>
private void BleCore_CharacteristicAdded(Windows.Devices.Bluetooth.GenericAttributeProfile.GattCharacteristic gattCharacteristic)
{
RunAsync(() =>
{
// this.cmbFeatures.Items.Add(gattCharacteristic.Uuid);
this.GattCharacteristics.Add(gattCharacteristic);
this.btnOpteron.Enabled = true;
});
}
private void Init()
{
this.btnConnect.Enabled = false;
this.btnOpteron.Enabled = false;
this.btnLoader.Enabled = true;
this.btnSend.Enabled = false;
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
this.bleCore.MessAgeChanged -= BleCore_MessAgeChanged;
this.bleCore.DeviceWatcherChanged -= BleCore_DeviceWatcherChanged;
//this.bleCore.GattDeviceServiceAdded -= BleCore_GattDeviceServiceAdded;
//this.bleCore.CharacteristicAdded -= BleCore_CharacteristicAdded;
this.bleCore.Dispose();
}
private void Ymodem_ProgressChanged(int percent, string status)
{
RunAsync(() => AppendLog($"[进度] {percent}% - {status}"));
}
private void Ymodem_LogMessage(string msg)
{
RunAsync(() => AppendLog($"[上位机] {msg}"));
}
private void Ymodem_ErrorOccurred(string error)
{
RunAsync(() =>
{
AppendLog($"[错误] {error}");
MessageBox.Show(error, "Ymodem 错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
});
}
private void btnServer_Click(object sender, EventArgs e)
{
}
private void btnFeatures_Click(object sender, EventArgs e)
{
}
}
}
3、YmodemSender.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.IO.Ports;
using System.Threading;
namespace TCM_OTA_APP
{
class YmodemSender
{
private BleCore _bleCore;
private int _fileSize;
private int _sentBytes;
private byte[] _fileData;
private string _fileName;
private bool _cancel;
// 事件
public event Action<int, string> ProgressChanged;
public event Action<string> LogMessage;
public event Action<string> ErrorOccurred;
private const byte SOH = 0x01;
private const byte STX = 0x02;
private const byte EOT = 0x04;
private const byte ACK = 0x06;
private const byte NAK = 0x15;
private const byte CAN = 0x18;
private const byte C = 0x43;
private const int MAX_RETRIES = 30;
private const int MAX_BLE_PACKET_SIZE = 10; // 默认 MTU,实际可根据协商调整
private const int BLE_SEND_DELAY_MS = 50; // 增加延时到30ms
// 用于同步等待应答
//private ManualResetEventSlim _ackEvent = new ManualResetEventSlim(false);
private byte _lastReceivedByte;
private bool _gotResponse;
private int _maxBlePacketSize;
public YmodemSender(BleCore bleCore)
{
_bleCore = bleCore;
// 订阅 Notify 数据接收
_bleCore.MessAgeChanged += OnBleDataReceived;
_maxBlePacketSize = _bleCore.GetMaxPacketSize();
Log($"📊 BLE 最大包大小: {_maxBlePacketSize} 字节");
}
private void OnBleDataReceived(MsgType type, string message, byte[] data)
{
if (type == MsgType.BleData && data != null && data.Length > 0)
{
_lastReceivedByte = data[0];
_gotResponse = true; // 仅设置标志,等待循环会轮询此标志
}
}
/// <summary>
/// 发送文件
/// </summary>
public async Task SendFileAsync(string filePath)
{
// 1. 先发送自定义命令使板端进入 Ymodem 模式
Log("正在唤醒设备进入 Ymodem 模式...");
if (!await SendEnterYmodemCommandAsync())
throw new Exception("板端未响应进入 Ymodem 模式命令");
// 2. 以下为标准 Ymodem 发送流程(与原先一致)
if (!File.Exists(filePath))
throw new FileNotFoundException("文件不存在", filePath);
_fileData = File.ReadAllBytes(filePath);
_fileSize = _fileData.Length;
_fileName = Path.GetFileName(filePath);
_cancel = false;
DateTime startTime = DateTime.Now;
Log($"⏱️ 开始发送时间: {startTime:HH:mm:ss.fff}");
Log($"开始发送文件: {_fileName}, 大小: {_fileSize} 字节");
// 等待接收方发送 'C' (CRC模式)
Log("等待接收方 CRC 启动信号...");
if (!await WaitForCharAsync(C, 60000))
throw new TimeoutException("未收到启动信号 (C)");
// 3. 发送文件名和大小 (0号包)
Log("发送文件信息包...");
await SendFileInfoPacketAsync(_fileName, _fileSize);
if (!await WaitForAckAsync())
throw new Exception("文件信息包未确认");
// 4. 发送数据包
Log($"开始发送数据,总大小: {_fileSize} 字节");
_sentBytes = 0;
int packetNumber = 1;
int offset = 0;
int maxPacketSize = 128; // Ymodem 标准包大小
try
{
while (offset < _fileSize && !_cancel)
{
Log($"🔁 循环开始: packetNumber={packetNumber}, offset={offset}/{_fileSize}");
int chunkSize = Math.Min(maxPacketSize, _fileSize - offset);
byte[] chunk = new byte[chunkSize];
Array.Copy(_fileData, offset, chunk, 0, chunkSize);
bool ack = false;
int retries = 0;
while (!ack && retries < MAX_RETRIES && !_cancel)
{
try
{
await SendDataPacketAsync(chunk, packetNumber);
ack = await WaitForAckAsync();
}
catch (Exception ex)
{
Log($"发送或等待异常: {ex.Message},将重试");
ack = false;
}
if (!ack)
{
Log($"⚠️ 数据包 #{packetNumber} 未确认,重试 ({retries + 1}/{MAX_RETRIES})");
retries++;
await Task.Delay(300); // 重试前等待稍长
}
else
{
Log($"✅ 数据包 #{packetNumber} 已确认");
}
}
if (!ack)
throw new Exception($"数据包 #{packetNumber} 发送失败,超过最大重试次数");
offset += chunkSize;
_sentBytes = offset;
int percent = (int)((double)offset / _fileSize * 100);
ProgressChanged?.Invoke(percent, $"发送中 {percent}%");
Log($"📈 准备递增 packetNumber,当前值={packetNumber}");
packetNumber++;
Log($"📈 packetNumber 递增后 = {packetNumber}");
}
DateTime endTime = DateTime.Now;
TimeSpan elapsed = endTime - startTime;
Log($"✅ 发送完成,总耗时: {elapsed.TotalSeconds:F2} 秒 ({elapsed.TotalMilliseconds:F0} ms)");
ProgressChanged?.Invoke(100, $"完成 (耗时 {elapsed.TotalSeconds:F2}s)");
}
catch (Exception ex)
{
DateTime endTime = DateTime.Now;
TimeSpan elapsed = endTime - startTime;
Log($"❌ 发送失败,耗时: {elapsed.TotalSeconds:F2} 秒,错误: {ex.Message}");
throw; // 或处理
}
if (_cancel)
{
Log("发送被取消");
return;
}
// 5. 发送 EOT
Log("所有数据发送完毕,发送 EOT...");
await SendEotAsync(); // 内部已包含重试
// 6. 发送结束包
Log("发送结束包...");
bool endAck = false;
int endRetries = 0;
int maxEndRetries = 3; // 只尝试3次
while (!endAck && endRetries < maxEndRetries && !_cancel)
{
try
{
await SendEndPacketAsync();
endAck = await WaitForAckAsync();
if (!endAck)
{
Log($"⚠️ 结束包未确认,重试 ({endRetries + 1}/{maxEndRetries})");
endRetries++;
await Task.Delay(300);
}
}
catch (Exception ex)
{
Log($"⚠️ 结束包异常: {ex.Message},继续重试");
endRetries++;
await Task.Delay(300);
}
}
if (!endAck)
{
Log("⚠️ 结束包未确认,但 EOT 已确认,认为传输成功");
}
else
{
Log("✅ 结束包已确认");
}
Log("Ymodem 发送完成");
ProgressChanged?.Invoke(100, "完成");
}
/// <summary>
/// 取消发送
/// </summary>
public void Cancel()
{
_cancel = true;
Log($"取消发送数据:AAAAA");
//_ackEvent.Set(); // 唤醒等待
}
// ---------- 底层 BLE 发送与接收 ----------
private async Task SendRawDataAsync(byte[] data)
{
try
{
int offset = 0;
while (offset < data.Length)
{
int len = Math.Min(_maxBlePacketSize, data.Length - offset);
byte[] chunk = new byte[len];
Array.Copy(data, offset, chunk, 0, len);
await _bleCore.Write(chunk);
offset += len;
//await Task.Delay(BLE_SEND_DELAY_MS); // 确保模块有足够时间处理
}
}
catch (Exception ex)
{
Log($"发送原始数据异常: {ex.Message}");
throw;
}
}
private async Task SendPacketAsync(byte header, byte packetNumber, byte[] data)
{
// 计算 CRC
ushort crc = Crc16.CalcCrc(data);
byte[] crcBytes = new byte[] { (byte)(crc >> 8), (byte)(crc & 0xFF) };
// 组装完整 Ymodem 包
List<byte> packet = new List<byte>();
packet.Add(header);
packet.Add(packetNumber);
packet.Add((byte)(~packetNumber));
packet.AddRange(data);
packet.AddRange(crcBytes);
await SendRawDataAsync(packet.ToArray());
}
private async Task SendFileInfoPacketAsync(string fileName, int fileSize)
{
byte[] nameBytes = System.Text.Encoding.ASCII.GetBytes(fileName);
byte[] sizeBytes = System.Text.Encoding.ASCII.GetBytes(fileSize.ToString());
int packetLen = 128;
byte[] packetData = new byte[packetLen];
Array.Copy(nameBytes, 0, packetData, 0, Math.Min(nameBytes.Length, packetLen - 1));
int pos = nameBytes.Length + 1;
if (pos < packetLen)
{
Array.Copy(sizeBytes, 0, packetData, pos, Math.Min(sizeBytes.Length, packetLen - pos - 1));
}
await SendPacketAsync(SOH, 0x00, packetData);
}
private async Task SendDataPacketAsync(byte[] data, int packetNumber)
{
if (_cancel) return; // 或者抛出异常
Log($"发送数据包 #{packetNumber}, 大小: {data.Length}");
int dataLen = data.Length;
bool useStx = data.Length > 128;
byte header = useStx ? STX : SOH;
int maxLen = useStx ? 1024 : 128;
byte[] packetData = new byte[maxLen];
Array.Copy(data, packetData, dataLen); // data.Length 可能大于 128 但 maxLen 只有 128 → 越界!
// 填充 0x1A
for (int i = data.Length; i < maxLen; i++)
packetData[i] = 0x1A;
await SendPacketAsync(header, (byte)(packetNumber & 0xFF), packetData);
await Task.Delay(100); // 给模块处理时间
}
private async Task SendEndPacketAsync()
{
byte[] zeroData = new byte[128];
await SendPacketAsync(SOH, 0x00, zeroData);
}
private async Task SendEotAsync()
{
bool ack = false;
int retries = 0;
while (!ack && retries < MAX_RETRIES && !_cancel)
{
await SendRawDataAsync(new byte[] { EOT });
Log($"发送 EOT,等待确认...");
ack = await WaitForAckAsync();
if (!ack)
{
Log($"⚠️ EOT 未确认,重试 ({retries + 1}/{MAX_RETRIES})");
retries++;
await Task.Delay(300);
}
else
{
Log("✅ EOT 已确认");
}
}
if (!ack)
throw new Exception("EOT 发送失败,超过最大重试次数");
}
// ---------- 等待应答 ----------
private async Task<bool> WaitForCharAsync(byte expected, int timeoutMs)
{
using (var cts = new CancellationTokenSource(timeoutMs))
{
while (!cts.Token.IsCancellationRequested)
{
if (_gotResponse)
{
if (_lastReceivedByte == expected)
return true;
if (_lastReceivedByte == CAN)
throw new Exception("接收方发送 CAN 取消");
// 收到其他字节,重置标志,继续等待
_gotResponse = false;
}
// 等待 50ms 后再次检查,同时监听取消请求
await Task.Delay(50, cts.Token).ConfigureAwait(false);
}
return false;
}
}
private async Task<bool> WaitForAckAsync()
{
int timeoutMs = 5000;
using (var cts = new CancellationTokenSource(timeoutMs))
{
while (!cts.Token.IsCancellationRequested)
{
if (_gotResponse)
{
byte b = _lastReceivedByte;
_gotResponse = false;
Log($"📥 WaitForAckAsync 收到字节: 0x{b:X2}");
if (b == ACK) return true;
if (b == CAN)
{
Log("接收方发送 CAN,终止传输");
return false; // 不再抛出异常
}
if (b == NAK) return false;
// 过滤蓝牙 状态字节(0xE0, 0x00 等),继续等待
if (b == 0xE0 || b == 0x00 || b > 0x18) // 非协议字节
{
Log($"忽略无效字节 0x{b:X2},继续等待...");
continue;
}
// 其他未定义字节也忽略
continue;
}
await Task.Delay(100, cts.Token).ConfigureAwait(false);
}
Log("⏰ WaitForAckAsync 超时");
return false;
}
}
private void Log(string msg)
{
LogMessage?.Invoke(msg);
}
// 计算 CRC16/Modbus
private static ushort CalcCrc16(byte[] data)
{
ushort crc = 0xFFFF;
for (int i = 0; i < data.Length; i++)
{
crc ^= data[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 0x0001) != 0)
crc = (ushort)((crc >> 1) ^ 0xA001);
else
crc >>= 1;
}
}
return crc;
}
/// <summary>
/// 发送自定义命令使板端进入 Ymodem 模式,并等待确认
/// 这是我定义的指令,具体的根据自己的来
/// </summary>
private async Task<bool> SendEnterYmodemCommandAsync()
{
// 构造命令报文 发给控制板的升级指令
List<byte> cmd = new List<byte>();
cmd.Add(0xEE);
cmd.Add(0x01);
cmd.Add(0x00);
cmd.Add(0x08);
cmd.Add(0x54);
// 计算 CRC16(从 VERSION 开始到 CMD 结束,即索引 2~4)
byte[] crcData = cmd.Skip(1).ToArray(); // {0x01, 0x00, 0x08, 0x54}
ushort crc = CalcCrc16(crcData);
cmd.Add((byte)(crc >> 8));
cmd.Add((byte)(crc & 0xFF));
cmd.Add(0xEE);
// 发送命令
await _bleCore.Write(cmd.ToArray());
Log("已发送进入 Ymodem 模式命令,等待确认...");
await _bleCore.Write(cmd.ToArray());
Log("已发送进入 Ymodem 模式命令(不等待响应)");
return true; // 始终认为成功
}
}
}
4、BleCore.cs
XXXX改成蓝牙名称
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Devices.Bluetooth;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
using Windows.Devices.Enumeration;
using Windows.Foundation;
using Windows.Networking;
using Windows.Networking.Proximity;
using Windows.Networking.Sockets;
using Windows.Security.Cryptography;
using Windows.Storage.Streams;
namespace TCM_OTA_APP
{
public enum MsgType
{
NotifyTxt,
BleData,
BleDevice
}
public class BleCore
{
private Boolean asyncLock = false;
/// <summary>
/// 搜索蓝牙设备对象
/// </summary>
private DeviceWatcher deviceWatcher;
/// <summary>
/// 当前连接的服务
/// </summary>
public GattDeviceService CurrentService { get; set; }
/// <summary>
/// 当前连接的蓝牙设备
/// </summary>
public BluetoothLEDevice CurrentDevice { get; set; }
/// <summary>
/// 写特征对象
/// </summary>
public GattCharacteristic CurrentWriteCharacteristic { get; set; }
/// <summary>
/// 通知特征对象
/// </summary>
public GattCharacteristic CurrentNotifyCharacteristic { get; set; }
/// <summary>
/// 特性通知类型通知启用
/// </summary>
private const GattClientCharacteristicConfigurationDescriptorValue CHARACTERISTIC_NOTIFICATION_TYPE = GattClientCharacteristicConfigurationDescriptorValue.Notify;
/// <summary>
/// 存储检测到的设备
/// </summary>
private List<BluetoothLEDevice> DeviceList = new List<BluetoothLEDevice>();
/// <summary>
/// 定义搜索蓝牙设备委托
/// </summary>
public delegate void DeviceWatcherChangedEvent(MsgType type, BluetoothLEDevice bluetoothLEDevice);
/// <summary>
/// 搜索蓝牙事件
/// </summary>
public event DeviceWatcherChangedEvent DeviceWatcherChanged;
/// <summary>
/// 获取服务委托
/// </summary>
public delegate void GattDeviceServiceAddedEvent(GattDeviceService gattDeviceService);
/// <summary>
/// 获取服务事件
/// </summary>
public event GattDeviceServiceAddedEvent GattDeviceServiceAdded;
/// <summary>
/// 获取特征委托
/// </summary>
public delegate void CharacteristicAddedEvent(GattCharacteristic gattCharacteristic);
/// <summary>
/// 获取特征事件
/// </summary>
public event CharacteristicAddedEvent CharacteristicAdded;
/// <summary>
/// 提示信息委托
/// </summary>
public delegate void MessAgeChangedEvent(MsgType type, string message, byte[] data = null);
/// <summary>
/// 提示信息事件
/// </summary>
public event MessAgeChangedEvent MessAgeChanged;
/// <summary>
/// 当前连接的蓝牙Mac
/// </summary>
public string CurrentDeviceMAC { get; set; }
public BleCore()
{
}
/// <summary>
/// 搜索蓝牙设备
/// </summary>
public void StartBleDeviceWatcher()
{
string[] requestedProperties = { "System.Devices.Aep.DeviceAddress", "System.Devices.Aep.IsConnected", "System.Devices.Aep.Bluetooth.Le.IsConnectable" };
string aqsAllBluetoothLEDevices = "(System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\")";
deviceWatcher =
DeviceInformation.CreateWatcher(aqsAllBluetoothLEDevices, requestedProperties, DeviceInformationKind.AssociationEndpoint);
// Register event handlers before starting the watcher.
deviceWatcher.Added += this.DeviceWatcher_Added;
deviceWatcher.Updated += DeviceWatcher_Updated;
deviceWatcher.Stopped += this.DeviceWatcher_Stopped;
deviceWatcher.Start();
string msg = "自动发现设备中..";
this.MessAgeChanged(MsgType.NotifyTxt, msg);
}
private void DeviceWatcher_Updated(DeviceWatcher sender, DeviceInformationUpdate args)
{
}
/// <summary>
/// 停止搜索蓝牙
/// </summary>
public void StopBleDeviceWatcher()
{
this.deviceWatcher.Stop();
}
/// <summary>
/// 获取发现的蓝牙设备
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private async void DeviceWatcher_Added(DeviceWatcher sender, DeviceInformation args)
{
string deviceName = args.Name ?? "";
if (deviceName.Contains("XXXX"))
{
// 👇 检查是否可连接(正在广播的设备)
if (args.Properties.ContainsKey("System.Devices.Aep.Bluetooth.Le.IsConnectable"))
{
bool isConnectable = (bool)args.Properties["System.Devices.Aep.Bluetooth.Le.IsConnectable"];
if (isConnectable)
{
this.MessAgeChanged(MsgType.NotifyTxt, $"发现目标设备(在线): {args.Id}");
await this.Matching(args.Id);
}
else
{
//this.MessAgeChanged(MsgType.NotifyTxt, $"发现目标设备(离线): {args.Name} - 已忽略");
}
}
else
{
// 如果没有 IsConnectable 属性,可能是不支持的旧设备,选择处理
this.MessAgeChanged(MsgType.NotifyTxt, $"发现目标设备(未知状态): {args.Name}");
await this.Matching(args.Id);
}
}
}
/// <summary>
/// 停止搜索蓝牙设备
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void DeviceWatcher_Stopped(DeviceWatcher sender, object args)
{
string msg = "自动发现设备停止";
this.MessAgeChanged(MsgType.NotifyTxt, msg);
}
/// <summary>
/// 匹配
/// </summary>
/// <param name="Device"></param>
public void StartMatching(BluetoothLEDevice Device)
{
this.CurrentDevice = Device;
}
/// <summary>
/// 获取蓝牙服务
/// </summary>
public async Task<bool> FindService()
{
/* this.CurrentDevice.GetGattServicesAsync().Completed = async (asyncInfo, asyncStatus) =>
{
if (asyncStatus == AsyncStatus.Completed)
{
var services = asyncInfo.GetResults().Services;
this.MessAgeChanged(MsgType.NotifyTxt, "GattServices size=" + services.Count);
foreach (GattDeviceService ser in services)
{
GattDeviceServiceAdded(ser);
}
this.MessAgeChanged(MsgType.NotifyTxt, "获取服务码收集完毕");
}
};*/
try
{
var servicesResult = await this.CurrentDevice.GetGattServicesAsync(BluetoothCacheMode.Uncached);
if (servicesResult.Status == GattCommunicationStatus.Success)
{
var services = servicesResult.Services;
this.MessAgeChanged(MsgType.NotifyTxt, $"GattServices size={services.Count}");
foreach (GattDeviceService ser in services)
{
GattDeviceServiceAdded(ser);
}
this.MessAgeChanged(MsgType.NotifyTxt, "获取服务码收集完毕");
return true;
}
else
{
this.MessAgeChanged(MsgType.NotifyTxt, $"获取服务失败: {servicesResult.Status}");
return false;
}
}
catch (Exception ex)
{
this.MessAgeChanged(MsgType.NotifyTxt, $"获取服务异常: {ex.Message}");
return false;
}
}
/// <summary>
/// 获取特性
/// </summary>
public async Task<bool> FindCharacteristic(GattDeviceService gattDeviceService)
{
/*this.CurrentService = gattDeviceService;
foreach (var c in gattDeviceService.GetAllCharacteristics())
{
this.CharacteristicAdded(c);
}
this.MessAgeChanged(MsgType.NotifyTxt, "获取特性码收集完毕");*/
try
{
this.CurrentService = gattDeviceService;
var characteristicsResult = await gattDeviceService.GetCharacteristicsAsync(BluetoothCacheMode.Uncached);
if (characteristicsResult.Status == GattCommunicationStatus.Success)
{
var characteristics = characteristicsResult.Characteristics;
this.MessAgeChanged(MsgType.NotifyTxt, $"获取到 {characteristics.Count} 个特征码");
foreach (var c in characteristics)
{
this.CharacteristicAdded(c);
}
this.MessAgeChanged(MsgType.NotifyTxt, "获取特性码收集完毕");
return true;
}
else
{
this.MessAgeChanged(MsgType.NotifyTxt, $"获取特性码失败: {characteristicsResult.Status}");
return false;
}
}
catch (Exception ex)
{
this.MessAgeChanged(MsgType.NotifyTxt, $"获取特性码异常: {ex.Message}");
return false;
}
}
/// <summary>
/// 获取操作
/// </summary>
/// <returns></returns>
public async Task<bool> SetOpteron(GattCharacteristic gattCharacteristic)
{
if (this.CurrentDevice != null)
{
this.MessAgeChanged(MsgType.NotifyTxt, $"设备名称: {this.CurrentDevice.Name}");
this.MessAgeChanged(MsgType.NotifyTxt, $"连接状态: {this.CurrentDevice.ConnectionStatus}");
this.MessAgeChanged(MsgType.NotifyTxt, $"设备ID: {this.CurrentDevice.DeviceId}");
}
/* if (gattCharacteristic.CharacteristicProperties == GattCharacteristicProperties.Write)
{
this.CurrentWriteCharacteristic = gattCharacteristic;
}
if (gattCharacteristic.CharacteristicProperties == GattCharacteristicProperties.Notify)
{
this.CurrentNotifyCharacteristic = gattCharacteristic;
}
if ((uint)gattCharacteristic.CharacteristicProperties == 26)
{ }
if (gattCharacteristic.CharacteristicProperties == (GattCharacteristicProperties.Notify | GattCharacteristicProperties.Read | GattCharacteristicProperties.Write))
{
this.CurrentWriteCharacteristic = gattCharacteristic;
this.CurrentNotifyCharacteristic = gattCharacteristic;
this.CurrentNotifyCharacteristic.ProtectionLevel = GattProtectionLevel.Plain;
this.CurrentNotifyCharacteristic.ValueChanged += Characteristic_ValueChanged;
await this.EnableNotifications(CurrentNotifyCharacteristic);
}
this.Connect();*/
try
{
// 设置写特征
if (gattCharacteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Write))
{
this.CurrentWriteCharacteristic = gattCharacteristic;
this.MessAgeChanged(MsgType.NotifyTxt, "已设置写特征");
}
// 👇 修改:只要支持 Notify 就启用通知(不要求同时支持 Read 和 Write)
if (gattCharacteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Notify))
{
this.CurrentNotifyCharacteristic = gattCharacteristic;
this.CurrentNotifyCharacteristic.ProtectionLevel = GattProtectionLevel.Plain;
this.CurrentNotifyCharacteristic.ValueChanged += Characteristic_ValueChanged;
this.MessAgeChanged(MsgType.NotifyTxt, "已注册通知事件,正在启用通知...");
await this.EnableNotifications(CurrentNotifyCharacteristic);
this.MessAgeChanged(MsgType.NotifyTxt, "通知启用完成");
}
else
{
this.MessAgeChanged(MsgType.NotifyTxt, "该特征不支持 Notify");
}
// 执行连接操作
return await this.Connect();
}
catch (Exception ex)
{
string msg = $"设置操作失败: {ex.Message}";
this.MessAgeChanged(MsgType.NotifyTxt, msg);
return false;
}
}
/// <summary>
/// 连接蓝牙
/// </summary>
/// <returns></returns>
private async Task<bool> Connect()
{
/* byte[] _Bytes1 = BitConverter.GetBytes(this.CurrentDevice.BluetoothAddress);
Array.Reverse(_Bytes1);
this.CurrentDeviceMAC = BitConverter.ToString(_Bytes1, 2, 6).Replace('-', ':').ToLower();
string msg = "正在连接设备<" + this.CurrentDeviceMAC + ">..";
this.MessAgeChanged(MsgType.NotifyTxt, msg);
this.CurrentDevice.ConnectionStatusChanged += this.CurrentDevice_ConnectionStatusChanged;*/
try
{
if (this.CurrentDevice == null)
{
this.MessAgeChanged(MsgType.NotifyTxt, "设备为空,无法连接");
return false;
}
// 获取MAC地址
byte[] _Bytes1 = BitConverter.GetBytes(this.CurrentDevice.BluetoothAddress);
Array.Reverse(_Bytes1);
this.CurrentDeviceMAC = BitConverter.ToString(_Bytes1, 2, 6).Replace('-', ':').ToLower();
// 检查当前连接状态
if (this.CurrentDevice.ConnectionStatus == BluetoothConnectionStatus.Connected)
{
string msg = $"设备<{this.CurrentDeviceMAC}>已连接";
this.MessAgeChanged(MsgType.NotifyTxt, msg);
return true;
}
string connectMsg = $"正在连接设备<{this.CurrentDeviceMAC}>..";
this.MessAgeChanged(MsgType.NotifyTxt, connectMsg);
// 注册连接状态变化事件
this.CurrentDevice.ConnectionStatusChanged += this.CurrentDevice_ConnectionStatusChanged;
// 尝试通过获取服务来触发连接
var servicesResult = await this.CurrentDevice.GetGattServicesAsync();
if (servicesResult.Status == GattCommunicationStatus.Success)
{
connectMsg = $"设备<{this.CurrentDeviceMAC}>连接成功";
this.MessAgeChanged(MsgType.NotifyTxt, connectMsg);
return true;
}
else
{
connectMsg = $"设备<{this.CurrentDeviceMAC}>连接失败: {servicesResult.Status}";
this.MessAgeChanged(MsgType.NotifyTxt, connectMsg);
return false;
}
}
catch (Exception ex)
{
string msg = $"连接异常: {ex.Message}";
this.MessAgeChanged(MsgType.NotifyTxt, msg);
return false;
}
}
/// <summary>
/// 搜索到的蓝牙设备
/// </summary>
/// <returns></returns>
private async Task Matching(string Id)
{
try
{
BluetoothLEDevice bleDevice = await BluetoothLEDevice.FromIdAsync(Id);
this.DeviceList.Add(bleDevice);
this.DeviceWatcherChanged(MsgType.BleDevice, bleDevice);
}
catch (Exception e)
{
string msg = "没有发现设备" + e.ToString();
this.MessAgeChanged(MsgType.NotifyTxt, msg);
this.StartBleDeviceWatcher();
}
}
/// <summary>
/// 主动断开连接
/// </summary>
/// <returns></returns>
public void Dispose()
{
CurrentDeviceMAC = null;
CurrentService?.Dispose();
CurrentDevice?.Dispose();
CurrentDevice = null;
CurrentService = null;
CurrentWriteCharacteristic = null;
CurrentNotifyCharacteristic = null;
MessAgeChanged(MsgType.NotifyTxt, "主动断开连接");
}
private async void CurrentDevice_ConnectionStatusChanged(BluetoothLEDevice sender, object args)
{
if (sender.ConnectionStatus == BluetoothConnectionStatus.Disconnected && CurrentDeviceMAC != null)
{
string msg = "设备已断开,自动重连";
MessAgeChanged(MsgType.NotifyTxt, msg);
if (!asyncLock)
{
asyncLock = true;
this.CurrentDevice.Dispose();
this.CurrentDevice = null;
CurrentService = null;
CurrentWriteCharacteristic = null;
CurrentNotifyCharacteristic = null;
await SelectDeviceFromIdAsync(CurrentDeviceMAC);
}
}
else
{
string msg = "设备已连接";
MessAgeChanged(MsgType.NotifyTxt, msg);
}
}
/// <summary>
/// 设置特征对象为接收通知对象
/// </summary>
/// <param name="characteristic"></param>
/// <returns></returns>
public async Task EnableNotifications(GattCharacteristic characteristic)
{
string msg = $"正在启用通知,设备状态: {CurrentDevice.ConnectionStatus}";
this.MessAgeChanged(MsgType.NotifyTxt, msg);
try
{
GattCommunicationStatus status = await characteristic.WriteClientCharacteristicConfigurationDescriptorAsync(CHARACTERISTIC_NOTIFICATION_TYPE);
if (status == GattCommunicationStatus.Success)
{
this.MessAgeChanged(MsgType.NotifyTxt, "✅ 通知启用成功");
}
else if (status == GattCommunicationStatus.Unreachable)
{
this.MessAgeChanged(MsgType.NotifyTxt, "❌ 设备不可达,通知启用失败");
if (CurrentNotifyCharacteristic != null && !asyncLock)
{
asyncLock = true;
await this.EnableNotifications(CurrentNotifyCharacteristic);
asyncLock = false;
}
}
else
{
this.MessAgeChanged(MsgType.NotifyTxt, $"❌ 通知启用失败: {status}");
}
}
catch (Exception ex)
{
this.MessAgeChanged(MsgType.NotifyTxt, $"❌ 通知启用异常: {ex.Message}");
}
}
/// <summary>
/// 接受到蓝牙数据
/// </summary>
private void Characteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args)
{
byte[] data;
CryptographicBuffer.CopyToByteArray(args.CharacteristicValue, out data);
string str = BitConverter.ToString(data);
// 👇 添加调试日志
this.MessAgeChanged(MsgType.NotifyTxt, $"[板端回复:]收到原始数据: {str}");
this.MessAgeChanged(MsgType.BleData, str, data);
}
/// <summary>
/// 发送数据接口
/// </summary>
/// <returns></returns>
public async Task Write(byte[] data)
{
if (CurrentWriteCharacteristic != null)
{
await CurrentWriteCharacteristic.WriteValueAsync(CryptographicBuffer.CreateFromByteArray(data), GattWriteOption.WriteWithResponse);
string str = "发送数据:" + BitConverter.ToString(data);
this.MessAgeChanged(MsgType.BleData, str, data);
}
}
/// <summary>
/// 按MAC地址查找系统中配对设备
/// </summary>
/// <param name="MAC"></param>
public async Task SelectDevice(string MAC)
{
CurrentDeviceMAC = MAC;
CurrentDevice = null;
var deviceInformation = await DeviceInformation.FindAllAsync(BluetoothLEDevice.GetDeviceSelector());
foreach (DeviceInformation di in deviceInformation)
{
await Matching(di.Id);
}
if (CurrentDevice == null)
{
string msg = "没有发现设备";
this.MessAgeChanged(MsgType.NotifyTxt, msg);
StartBleDeviceWatcher();
}
}
/// <summary>
/// 按MAC地址直接组装设备ID查找设备
/// </summary>
/// <param name="MAC"></param>
/// <returns></returns>
public async Task<BluetoothLEDevice> SelectDeviceFromIdAsync(string MAC)
{
CurrentDeviceMAC = MAC;
CurrentDevice = null;
try
{
// 将 MAC 字符串转换为 ulong (去掉冒号)
string hex = MAC.Replace(":", "");
if (ulong.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out ulong address))
{
// 直接通过蓝牙地址获取设备
var device = await BluetoothLEDevice.FromBluetoothAddressAsync(address);
if (device != null)
{
CurrentDevice = device;
this.DeviceWatcherChanged?.Invoke(MsgType.BleDevice, device);
// 尝试获取服务以触发连接
var servicesResult = await device.GetGattServicesAsync();
if (servicesResult.Status == GattCommunicationStatus.Success)
{
MessAgeChanged?.Invoke(MsgType.NotifyTxt, $"重连设备 {MAC} 成功");
return device;
}
else
{
MessAgeChanged?.Invoke(MsgType.NotifyTxt, $"重连失败: {servicesResult.Status}");
return null;
}
}
}
// 若直接获取失败,则枚举已配对设备(备用方案)
var devices = await DeviceInformation.FindAllAsync(BluetoothLEDevice.GetDeviceSelector());
foreach (var di in devices)
{
var device = await BluetoothLEDevice.FromIdAsync(di.Id);
if (device != null)
{
byte[] bytes = BitConverter.GetBytes(device.BluetoothAddress);
Array.Reverse(bytes);
string mac = BitConverter.ToString(bytes, 2, 6).Replace('-', ':').ToLower();
if (mac == MAC.ToLower())
{
CurrentDevice = device;
this.DeviceWatcherChanged?.Invoke(MsgType.BleDevice, device);
var servicesResult = await device.GetGattServicesAsync();
if (servicesResult.Status == GattCommunicationStatus.Success)
{
MessAgeChanged?.Invoke(MsgType.NotifyTxt, $"重连设备 {MAC} 成功");
return device;
}
else
{
MessAgeChanged?.Invoke(MsgType.NotifyTxt, $"重连失败: {servicesResult.Status}");
return null;
}
}
}
}
MessAgeChanged?.Invoke(MsgType.NotifyTxt, "未找到该MAC设备,启动扫描...");
StartBleDeviceWatcher();
return null;
}
catch (Exception ex)
{
MessAgeChanged?.Invoke(MsgType.NotifyTxt, $"重连异常: {ex.Message}");
return null;
}
}
public int GetMaxPacketSize()
{
try
{
if (CurrentService != null)
{
var session = CurrentService.Session;
if (session != null)
{
int mtu = session.MaxPduSize;
if (mtu > 0)
return mtu - 3; // 减去 ATT 头部
}
}
}
catch { }
// 默认返回模块支持的最大值 244
return 20;
}
}
}
5、结束语
后续控制板的Bootloader 和APP跳转这些需要自己去钻研了,包括Ymodem协议的数据接收,包括地址的分配等;我的思路是:APP接收到升级跳转指令,APP程序跳转到Bootloader程序,跳转后等待升级(升级超时机制,升级重试机制等等),这些网上一堆,我主要想分享上位机的程序开发,希望能够对各位有所帮助。

363

被折叠的 条评论
为什么被折叠?



