【原创工具】蓝牙串口无线烧写STM32程序的工具,支持HC-05和CC2541两种蓝牙模块

【下载链接】

[Windows版UARTDFUv1.0]
链接:https://pan.baidu.com/s/1uIOV712tmQjW-zmbKTPvGg
提取码:3qpt

[安卓版UARTDFUv1.0]
链接:https://blog.csdn.net/ZLK1214/article/details/119111672

[适用于STM32F103和STM32F107的Bootloader]
链接:https://blog.csdn.net/ZLK1214/article/details/119087488

[适用于STM32F405RG的Bootloader]
链接:https://blog.csdn.net/ZLK1214/article/details/119062947

[其他Bootloader]
想要其他芯片的欢迎在评论区留言。

【工具说明】

此工具采用C语言开发(纯C语言实现上位机,没有类,全都是函数),开放源代码,开发工具为Visual Studio 2010, 编译出来的程序可在Windows XP及以上的系统中运行。
(也可以用更高版本的Visual Studio编译,但是版本太高的话,编译出来的程序就无法在XP系统中运行了)

【原创工具】蓝牙串口无线烧写STM32程序的工具,支持HC-05和CC2541两种蓝牙模块_第1张图片

支持下载hex格式的程序。选择好hex格式的程序文件后,可以看到程序的大小,程序的Flash地址范围,程序的入口地址。

程序可通过HC-05或CC2541蓝牙串口下载到STM32单片机中,当然直接用有线串口(USB转串口模块,或RS232线)下载也是可以的。

兼容性:
(1)带热缩管的3.6~6V电压范围的蓝色底板HC-05模块(3.3V供电不能工作)

【原创工具】蓝牙串口无线烧写STM32程序的工具,支持HC-05和CC2541两种蓝牙模块_第2张图片

(2)不带热缩管的3.2~6V电压范围的绿色底板HC-05模块(3.3V供电可以工作)

【原创工具】蓝牙串口无线烧写STM32程序的工具,支持HC-05和CC2541两种蓝牙模块_第3张图片

(3)不带热缩管的BT05-A蓝色小CC2541F256模块(3.3V供电可以工作)

【原创工具】蓝牙串口无线烧写STM32程序的工具,支持HC-05和CC2541两种蓝牙模块_第4张图片

【原创工具】蓝牙串口无线烧写STM32程序的工具,支持HC-05和CC2541两种蓝牙模块_第5张图片

(4)带热缩管的蓝色大CC2541F256模块,6针排针引出,线序跟HC-05相同(3.3V供电可以工作)

【原创工具】蓝牙串口无线烧写STM32程序的工具,支持HC-05和CC2541两种蓝牙模块_第6张图片

模块 蓝牙名称长度 蓝牙密码长度 备注
1 1~32 1~8 蓝牙名称可包含汉字,但不能以汉字结尾,否则最后一个汉字会乱码
2 1~32 1~16

1. 蓝牙名称不能含有汉字

2. 不能用AT+NAME?指令查询蓝牙名称

3. 不支持AT+INQ或AT+SCAN指令搜索蓝牙设备

3 1~18 6
4 1~20 6
编码为UTF-8,一个汉字算3个字符

测试发现,模块3和4虽然都是CC2541F256,但是他们的AT命令却有差别。
比如,模块3支持AT+ADDR和AT+LADDR命令,获取到的蓝牙地址相同,且主从模式下都能获取蓝牙名称和地址。但模块4不支持AT+ADDR命令,而且只有在从模式下才能通过AT+LADDR命令获取到蓝牙地址,AT+NAME名称也只能在从模式下获取。还有,模块3发送AT+BAUD8后返回的是+BAUD=8,但模块4返回的却是+BAUD=115200。模块3的AT命令不区分大小写,但模块4严格区分大小写,AT不能写成at,否则串口无回应。经测试,模块3和模块4可以互连。

【原创工具】蓝牙串口无线烧写STM32程序的工具,支持HC-05和CC2541两种蓝牙模块_第7张图片

 【原创工具】蓝牙串口无线烧写STM32程序的工具,支持HC-05和CC2541两种蓝牙模块_第8张图片

HC-05蓝牙模块的特点:

(1)体积较大,程序下载速度快。

(2)带蓝牙功能的笔记本电脑可以直接连接,但连接蓝牙串口后必须要保持连接1~2秒以上才能关闭,否则蓝牙模块有很大的概率死机,再也无法连接。
         关闭蓝牙串口后需要等1~2秒后才能再次打开,否则蓝牙模块不会死机,但提示串口打开失败。

(3)模块上带有KEY按键。
         松开KEY键给蓝牙模块通电,进入的是通信模式,波特率可调,此时不能响应AT指令
         如果配置的是主机模式,则通电后自动连接另一个蓝牙模块。两个HC-05蓝牙模块相连,配对密码必须相同,才能连接成功。蓝牙模块主动连接电脑时,电脑上打开传入(Incoming)COM口即可收发串口数据。
         如果配置的是从机模式,则等待其他蓝牙设备连接。电脑主动连接蓝牙模块,应打开传出(Outgoing)COM口
         按住KEY键给蓝牙模块通电,进入的是AT模式,波特率固定为38400,可以响应AT指令,但不能通信。要想通信,必须要松开KEY键重新上电。

(4)搜索蓝牙设备时可以搜出蓝牙名称和蓝牙地址。

(5)有些模块的供电电压是3.6~6V,有些是3.2V~6V,所以不是所有的模块都支持3.3V供电电压。

CC2541蓝牙模块的特点:

(1)体积较小,但是程序下载速度慢,一秒钟只能传输500字节数据。

(2)带蓝牙功能的笔记本电脑不能直接连接,必须额外插上USB转串口模块+CC2541模块。

(3)模块上没有KEY按键,上电后进入的就是AT模式,用AT+CONN命令连接另一个CC2541蓝牙模块,进入通信模式。AT模式和通信模式的波特率相同且可调。要退出通信模式,只能重新上电。

(4)搜索蓝牙设备时只能搜出蓝牙地址。

(5)模块的供电电压为3.3V。

【原创工具】蓝牙串口无线烧写STM32程序的工具,支持HC-05和CC2541两种蓝牙模块_第9张图片

【原创工具】蓝牙串口无线烧写STM32程序的工具,支持HC-05和CC2541两种蓝牙模块_第10张图片

安卓手机上可选择Serial Bluetooth Terminal软件连接蓝牙模块,查看串口调试信息。
在BLUETOOTH CLASSIC选项卡中可以搜索并连接HC-05蓝牙模块。
在BLUETOOTH LE选项卡中可以搜索并连接CC2541蓝牙模块。

【原创工具】蓝牙串口无线烧写STM32程序的工具,支持HC-05和CC2541两种蓝牙模块_第11张图片

【软件使用说明】

注意:为了简单起见,程序里面将蓝牙模块通信模式下的波特率写死了,固定为115200。使用新蓝牙模块,配置蓝牙时会询问是否将波特率改为115200,如果不改就不能继续后续操作。想要使用其他波特率,那就必须修改上位机的C语言源代码。

Tera Term串口调试工具的命令:

"C:\Program Files (x86)\teraterm\ttermpro.exe" /C=%d /SPEED=115200

(方案一)用笔记本电脑自带的蓝牙功能连接开发板上的HC-05蓝牙模块

1. 用四根杜邦线将一个USB转串口模块和HC-05蓝牙模块连接在一起(按下表),按住KEY按键插入电脑USB口。使用软件的蓝牙配置功能(串口号选择USB转串口模块),配置好蓝牙名称和配对密码,搜索设备,选中“空设备”并连接(将模块配置为从机模式)。

USB转串口模块 HC-05蓝牙模块
+5V VCC
GND GND
RXD TXD
TXD RXD

提示:有的HC-05蓝牙模块不支持“AT+NAME?”查询,因此无法查询出蓝牙设备名称,只能用“AT+NAME=XXX”设置蓝牙名称。

2. 拔出USB转串口模块,取下HC-05蓝牙模块,将HC-05蓝牙模块插到开发板上,确保开发板里面已用STLINK调试器(或JLINK调试器)烧写好bootloader程序。

3. 在电脑上添加蓝牙设备,搜索HC-05蓝牙模块,输入密码并配对,完成后会增加两个COM口。一个是传入(Incoming),另一个是传出(Outgoing)。

3. 在软件里面选择好要下载的hex文件,勾选上“连接最小保持时间”的复选框,毫秒值为2000。串口号选择传出COM口,点击下载按钮,即可下载程序到开发板的Application区域。

4. 点击“复位并运行固件“按钮,即可开始运行程序,并自动打开串口调试工具,查看蓝牙串口printf输出。

(方案二)电脑上插入USB转串口模块+HC-05蓝牙模块,连接插在开发板上的另一个HC-05蓝牙模块

1. 把开发板上的蓝牙模块取下来,用杜邦线将USB转串口模块和开发板上的这个蓝牙模块连接在一起,按住KEY按键插入电脑USB口。使用软件的蓝牙配置功能(串口号选择USB转串口模块),配置好蓝牙名称和配对密码,搜索设备,选中“空设备”并连接(将模块配置为从机模式)。

2. 把USB转串口模块和蓝牙模块拔下来,将蓝牙模块插回开发板。

3. 再拿另外一个蓝牙模块,用杜邦线连接到USB转串口模块上后,按住KEY按键插入电脑USB口。使用软件的蓝牙配置功能(串口号选择USB转串口模块),配置好蓝牙名称和配对密码,配对密码一定要和开发板上的蓝牙模块相同。搜索设备,找到开发板上的蓝牙名称并连接。

4. 拔出USB转串口模块,松开蓝牙模块上的KEY按键重新插入电脑USB口。

5. 在软件里面选择好要下载的hex文件,取消勾选“连接最小保持时间”的复选框。串口号选择USB转串口模块,点击下载按钮,即可下载程序到开发板的Application区域。

6. 点击“复位并运行固件“按钮,即可开始运行程序,并自动打开串口调试工具,查看蓝牙串口printf输出。

注意:这是一个不可靠的蓝牙传输信道,传输大块数据会发生字节传输错误、字节丢失的情况,导致出现Data CRC failed和Data timeout错误。不过不用担心,bootloader程序具有重传机制,重传出错的数据包。
如果发现上位机一直显示“请按下复位键”,无法开始下载程序,则说明bootloader程序main函数的dfu_cnt变量初值太小,将其调大就能解决问题。另外,还可以调大dfu_sync函数
的HAL_UART_Receive超时时间。传输信道越不可靠,这个值就应该设得越大。数值越大,上位机检测到设备的成功率越高,但开机后程序启动越慢。

(方案三)电脑上插入USB转串口模块+CC2541蓝牙模块,连接插在开发板上的另一个CC2541蓝牙模块

1. 把开发板上的蓝牙模块取下来,用杜邦线将USB转串口模块和开发板上的这个蓝牙模块连接在一起,插入电脑USB口。使用软件的蓝牙配置功能(串口号选择USB转串口模块),点击搜索设备,记录下窗口下方显示的本设备的(不是搜出来的其他设备的)蓝牙设备地址。蓝牙名称和配对密码不用管。

2. 把USB转串口模块和蓝牙模块拔下来,将蓝牙模块插回开发板。

3. 再拿另外一个蓝牙模块,用杜邦线连接到USB转串口模块上后,插入电脑USB口。使用软件的蓝牙配置功能(串口号选择USB转串口模块),搜索设备,找到开发板上的蓝牙地址并连接。窗口下方的蓝牙名称和配对密码也不用管。

4. 在软件里面选择好要下载的hex文件,取消勾选“连接最小保持时间”的复选框。串口号选择USB转串口模块,点击下载按钮,即可下载程序到开发板的Application区域。

5. 点击“复位并运行固件“按钮,即可开始运行程序,并自动打开串口调试工具,查看蓝牙串口printf输出。

注意:这是一个不可靠的蓝牙传输信道,传输大块数据会发生字节丢失的情况,导致出现Data timeout错误。不过不用担心,bootloader程序具有重传机制,重传出错的数据包。

【工作原理】

本工具把STM32的Flash空间分成了三个区域:Bootloader、Reserved和Application。

Bootloader区域存放是单片机复位后运行的第一个程序,负责接收蓝牙模块的程序数据和指令,烧写程序,并跳转到Application程序区运行。

Reserved区域用于保存程序的大小和CRC校验和,Application区域保存烧写的程序。

Bootloader和Application是两个不同的程序。Bootloader是固定不变的程序,采用HAL库编写。Application是通过蓝牙烧写的程序,除了HAL库外,还可以采用标准库,或者纯寄存器方式编写。Bootloader跳转到Application区执行前,会调用HAL_DeInit()函数,关闭所有的外设和中断,因此Application程序的运行不会受到Bootloader的影响(除了SysTick和IWDG)。因此,如果Application程序不是用HAL库写的,而是标准库或者纯寄存器操作,那么就必须要声明一个空的void SysTick_Handler(void)函数。

【关于字符编码】

蓝牙串口采用的字符编码为UTF-8,但Windows系统里面采用的编码是UTF-16。
UTF-8和UTF-16的区别:字符编码值相同,只是存储方式不同。UTF-8每个字符所占的字节数为1~4,属于变长存储。UTF-16绝大多数字符(基本多语言平面的字符)占2字节,属于定长存储。
另外,像GB2312这类特定语言的字符集统称为ANSI。

GetWindowText是一个宏,用于获取文本框输入的文字内容。根据项目配置(_UNICODE宏是否定义),GetWindowText宏展开后,要么是GetWindowTextA函数,要么是GetWindowTextW函数。Edit_GetText宏和GetWindowText宏等价。
GetWindowTextA函数获取到的字符编码为ANSI,在简体中文版Windows系统下是GB2312编码,使用字符数组的类型为char []。
GetWindowTextW函数获取到的字符编码为UTF-16,使用的字符数组的类型为wchar_t []。

char []字符数组可以存放任意编码的字符串。对于char str[] = "简体中文",源文件是什么编码,str字符数组就以什么编码保存字符串。
wchar_t []字符数组专门用来存放UTF-16编码的字符串。对于wchar_t wstr[] = L"简体中文",无论源文件是什么编码,wstr字符数组都是UTF-16编码。如果源文件不是UTF-16编码,编译器在编译时,会从源文件的字符串编码转换到UTF-16编码。

ANSI或UTF-8转UTF-16使用的函数是MultiByteToWideChar,反过来则使用WideCharToMultiByte。可以先获取转换后字符串的长度,用malloc函数分配好空间,再开始转换。

类型 未定义_UNICODE 定义了_UNICODE
TCHAR char wchar_t
LPSTR char *
LPCSTR const char *
LPWSTR wchar_t *
LPCWSTR const wchar_t *
LPTSTR char * wchar_t *
LPCTSTR const char * const wchar_t *

给TCHAR字符数组赋值要用TEXT()宏:TCHAR tstr[] = TEXT("简体中文")

【程序主要代码】

main.c:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "bluetooth.h"
#include "hexfile.h"
#include "port.h"
#include "taskbar.h"
#include "resource.h"
#include "UARTDFU.h"

#pragma comment(lib, "comctl32.lib")
#pragma comment(linker, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' language='*' publicKeyToken='6595b64144ccf1df'\"")

static void browse_files(void);
static DWORD CALLBACK browse_files_thread(LPVOID lpParameter);
static uint8_t calc_crc8(const void *data, int len);
static BOOL confirm_cancel(void);
static void download_firmware(uintptr_t header);
static DWORD CALLBACK download_firmware_process(LPVOID lpParameter);
static void enable_controls(BOOL enabled);
static int load_commports(void);
static void set_progress(int value);
static void update_fileinfo(const HexFile *hexfile);
static INT_PTR CALLBACK DlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
#if ADVANCED_MODE
static void load_configuration(void);
static void reset_and_run(int reset);
static DWORD CALLBACK reset_and_run_process(LPVOID lpParameter);
static void save_configuration(void);
#endif

HINSTANCE hinstMain;
HWND hwndCheck[2], hwndCombo, hwndSpin;
TCHAR szMessage[500];
static int downloading;
static BOOL file_loaded;
static HANDLE hThread;
static HWND hwndButton[7], hwndEdit[3], hwndDlg, hwndProgress;
#if ADVANCED_MODE
static int defport;
static BOOL conf_changed;
#endif

/* 弹出选择固件文件的对话框 */
static void browse_files(void)
{
	// GetOpenFileNameA函数不能在COINIT_MULTITHREADED环境下运行, 否则XP系统下“我的电脑”显示不出任何内容
	// 新建一个线程单独运行,可解决这个问题
	CreateThread(NULL, 0, browse_files_thread, NULL, 0, NULL);
}

static DWORD CALLBACK browse_files_thread(LPVOID lpParameter)
{
	char name[MAX_PATH];
	int ret;
	BOOL bret;
	HexFile hexfile;
	OPENFILENAMEA ofn = {0};

	name[0] = '\0';
	ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_NOCHANGEDIR;
	ofn.hInstance = hinstMain;
	ofn.hwndOwner = hwndDlg;
	ofn.lStructSize = sizeof(OPENFILENAMEA);
	ofn.lpstrDefExt = "hex";
	ofn.lpstrFile = name;
#if ADVANCED_MODE
	ofn.lpstrFilter = "HEX文件 (*.hex)\0*.HEX\0所有文件 (*.*)\0*.*\0";
#else
	ofn.lpstrFilter = "固件文件 (*.hex)\0*.HEX\0";
#endif
	ofn.nMaxFile = sizeof(name);
	bret = GetOpenFileNameA(&ofn);
	if (bret)
	{
		ret = HexFile_Load(&hexfile, name);
		if (ret == 0)
		{
			file_loaded = TRUE;
			update_fileinfo(&hexfile);
			HexFile_Free(&hexfile);
		}
		else
		{
			MessageBox(hwndDlg, TEXT("读取文件失败!"), TEXT("打开文件"), MB_ICONWARNING);
			file_loaded = FALSE;
			update_fileinfo(NULL);
		}
		SetWindowTextA(hwndEdit[0], name);
	}
	return bret;
}

/* 计算CRC8校验码 */
static uint8_t calc_crc8(const void *data, int len)
{
	const uint8_t *p = data;
	int i, j;
	uint16_t temp = 0;

	if (len != 0)
		temp = p[0] << 8;

	for (i = 1; i <= len; i++)
	{
		if (i != len)
			temp |= p[i];
		for (j = 0; j < 8; j++)
		{
			if (temp & 0x8000)
				temp ^= POLYNOMIAL_CRC8 << 7;
			temp <<= 1;
		}
	}
	return temp >> 8;
}

/* 确认取消下载 */
static BOOL confirm_cancel(void)
{
	if (downloading == 2)
		return MessageBox(hwndDlg, TEXT("您确定要取消固件下载吗?\n取消后设备将不能正常启动,但可以重新下载固件。"), TEXT("取消下载"), MB_ICONQUESTION | MB_OKCANCEL) == IDOK;
	else
		return TRUE;
}

/* 下载固件 */
static void download_firmware(uintptr_t header)
{
	if (downloading)
	{
		if (confirm_cancel())
		{
			downloading = 0;
			EnableWindow(hwndButton[0], FALSE);
#if ADVANCED_MODE
			EnableWindow(hwndButton[5], FALSE);
#endif
		}
	}
	else
	{
		downloading = 1;
		szMessage[0] = '\0';
		enable_controls(FALSE);
		hThread = CreateThread(NULL, 0, download_firmware_process, (LPVOID)header, 0, NULL);
	}
}

/* 下载固件的线程 */
static DWORD CALLBACK download_firmware_process(LPVOID lpParameter)
{
	char filename[MAX_PATH];
	double progress;
	int i, j, ret;
	uint8_t buffer[16];
	uint8_t crc;
	uintptr_t header = (uintptr_t)lpParameter;
	COMMTIMEOUTS timeouts = {0};
	DeviceResponse resp;
	FirmwareInfo info;
	HANDLE hPort;
	HexFile hexfile;
#if ADVANCED_MODE
	CRCConfig crccfg;
#endif

	// 初始化COM (用于设置任务栏图标上的进度条)
	CoInitializeEx(NULL, COINIT_MULTITHREADED);

	// 改为取消按钮
	if (header == HEADER_FIRMWARE_INFO)
	{
		Button_SetText(hwndButton[0], TEXT("取消"));
		EnableWindow(hwndButton[0], TRUE);
	}
#if ADVANCED_MODE
	else if (header == HEADER_CRC_CONFIG)
	{
		Button_SetText(hwndButton[5], TEXT("取消"));
		EnableWindow(hwndButton[5], TRUE);
	}
#endif

	// 打开串口
	set_progress(-1);
	hPort = open_port(-1);
	if (hPort == INVALID_HANDLE_VALUE)
		goto end;

	// 设置超时时间
	timeouts.ReadTotalTimeoutConstant = 80;
	timeouts.WriteTotalTimeoutConstant = 10000;
	SetCommTimeouts(hPort, &timeouts);

	// 检测设备
#if ADVANCED_MODE
	SetDlgItemText(hwndDlg, IDC_STATIC5, TEXT("请按下复位键"));
#else
	SetDlgItemText(hwndDlg, IDC_STATIC5, TEXT("正在检测设备..."));
#endif
	while (downloading)
	{
		memset(buffer, 0xab, sizeof(buffer));
		write_port(hPort, buffer, sizeof(buffer));
		
		ret = read_port(hPort, buffer, sizeof(buffer));
		if (ret == sizeof(buffer))
		{
			for (j = 0; j < sizeof(buffer); j++)
			{
				if (buffer[j] != 0xcd)
					break;
			}
			if (j == sizeof(buffer))
				break;
		}
	}
	if (!downloading)
		goto end;

	set_progress(0);
	timeouts.ReadTotalTimeoutConstant = 10000;
	SetCommTimeouts(hPort, &timeouts);

	switch (header)
	{
	case HEADER_FIRMWARE_INFO:
		// 读取HEX文件并发送HEX文件信息
		GetWindowTextA(hwndEdit[0], filename, sizeof(filename));
		ret = HexFile_Load(&hexfile, filename);
		if (ret == -1)
		{
			lstrcpy(szMessage, TEXT("读取固件文件失败!"));
			break;
		}
		update_fileinfo(&hexfile);

		info.header = HEADER_FIRMWARE_INFO;
		info.size = hexfile.size;
		info.start_addr = hexfile.start_addr;
		info.end_addr = hexfile.end_addr;
		info.entry_point = hexfile.entry_point;
		info.firmware_checksum = calc_crc8(hexfile.data, hexfile.size);
		info.header_checksum = calc_crc8(&info, sizeof(info) - 1);
		write_port(hPort, &info, sizeof(info));

		while (downloading)
		{
			ret = read_port(hPort, &resp, sizeof(resp));
			if (ret != sizeof(resp) || calc_crc8(&resp, sizeof(resp)) != 0)
			{
				lstrcpy(szMessage, TEXT("下载固件时发生错误!"));
				break;
			}
			else if (resp.addr == 0)
			{
				_stprintf_s(szMessage, _countof(szMessage), TEXT("固件大小超过设备Flash容量: %.1lfKB"), resp.size / 1024.0);
				break;
			}
			else if (resp.addr == 0xffffffff && resp.size == 0xffffffff)
			{
				// 重新发送固件信息
				write_port(hPort, &info, sizeof(info));
				continue;
			}
			else if (downloading == 1 && resp.addr != hexfile.start_addr)
			{
#if ADVANCED_MODE
				_stprintf_s(szMessage, _countof(szMessage), TEXT("固件地址无效!\n请将Keil MDK项目属性中Target选项卡下IROM1的值设为0x%08x,\n并将system_*.c中的VECT_TAB_OFFSET改为0x%04xU,\n然后重新编译固件。"), resp.addr, resp.addr - 0x08000000);
#else
				lstrcpy(szMessage, TEXT("该固件不适用于此设备!"));
#endif
				break;
			}
			else if (resp.size == 0)
				break; // 发送结束
			else if (resp.size == 0xffffffff)
				continue; // 继续等待 (比如擦除Flash需要很长时间, 为了避免提示超时, 可使用这个指令)
			else if (resp.addr - hexfile.start_addr + resp.size > hexfile.size)
			{
				lstrcpy(szMessage, TEXT("设备出现异常!"));
				break;
			}

			if (downloading == 1)
			{
				downloading = 2;
#if ADVANCED_MODE
				Button_SetCheck(hwndCheck[0], TRUE);
#endif
			}

			write_port(hPort, hexfile.data + (resp.addr - hexfile.start_addr), resp.size);
			progress = (double)(resp.addr - hexfile.start_addr + resp.size) / hexfile.size;
			i = (int)(progress * 100 + 0.5);
			if (i > 100)
				i = 100;
			set_progress(i);

			crc = calc_crc8(hexfile.data + (resp.addr - hexfile.start_addr), resp.size);
			write_port(hPort, &crc, 1);
		}

		HexFile_Free(&hexfile);
#if !ADVANCED_MODE
		// 下载完成后启动固件
		buffer[0] = 0xab;
		write_port(hPort, buffer, 1);
#endif
		break;
#if ADVANCED_MODE
	case HEADER_CRC_CONFIG:
		crccfg.header = HEADER_CRC_CONFIG;
		crccfg.crc_enabled = Button_GetCheck(hwndCheck[0]);
		crccfg.header_checksum = calc_crc8(&crccfg, sizeof(crccfg) - 1);
		do
		{
			write_port(hPort, &crccfg, sizeof(crccfg));
			ret = read_port(hPort, &resp, sizeof(resp));
			if (ret != sizeof(resp) || calc_crc8(&resp, sizeof(resp)) != 0)
				resp.size = 0; // 回应内容有误, 视为失败
		} while (downloading && resp.addr == 0xffffffff && resp.size == 0xffffffff); // 判断是否重传crccfg

		if (resp.size != 1)
			lstrcpy(szMessage, TEXT("修改CRC配置失败!"));
		break;
#endif
	}
	
end:
	close_port(hPort);
	if (hwndDlg != NULL)
		PostMessage(hwndDlg, WM_USER, 1, header);
	CoUninitialize();
	return 0;
}

/* 启用或禁用控件 */
static void enable_controls(BOOL enabled)
{
	EnableWindow(hwndButton[0], enabled && file_loaded);
	EnableWindow(hwndButton[1], enabled);
	EnableWindow(hwndButton[2], enabled);
	EnableWindow(hwndButton[4], enabled);
	EnableWindow(hwndCheck[1], enabled);
	EnableWindow(GetDlgItem(hwndDlg, IDC_STATIC13), enabled);
	EnableWindow(hwndCombo, enabled);
	EnableWindow(GetDlgItem(hwndDlg, IDC_STATIC9), enabled);
	EnableWindow(hwndEdit[2], enabled && Button_GetCheck(hwndCheck[1]));
	InvalidateRect(hwndSpin, NULL, FALSE);

#if ADVANCED_MODE
	EnableWindow(hwndButton[3], enabled);
	EnableWindow(hwndButton[5], enabled);
	EnableWindow(hwndButton[6], enabled);
	EnableWindow(hwndCheck[0], enabled);
	Edit_SetReadOnly(hwndEdit[1], !enabled);
#endif
}

/* 从注册表中读取串口列表 */
static int load_commports(void)
{
#if ADVANCED_MODE
	int selection;
#endif
	DWORD index, type, namelen, valuelen;
	HKEY key;
	LSTATUS status;
	TCHAR name[50];
	TCHAR value[50];

	ComboBox_ResetContent(hwndCombo);
	status = RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT("HARDWARE\\DEVICEMAP\\SERIALCOMM"), 0, KEY_READ, &key);
	if (status != ERROR_SUCCESS)
		return -1;

	index = 0;
	while (1)
	{
		namelen = _countof(name);
		valuelen = sizeof(value);
		status = RegEnumValue(key, index, name, &namelen, NULL, &type, (LPBYTE)value, &valuelen);
		if (status != ERROR_SUCCESS)
			break;
		ComboBox_AddString(hwndCombo, value);
		index++;
	}
	if (index != 0)
	{
#if ADVANCED_MODE
		_stprintf_s(value, _countof(value), TEXT("COM%d"), defport);
		selection = ComboBox_FindString(hwndCombo, 0, value);
		if (selection != -1)
			ComboBox_SetCurSel(hwndCombo, selection);
		else
#endif
			ComboBox_SetCurSel(hwndCombo, 0);
	}

	RegCloseKey(key);
	return 0;
}

/* 读取配置信息 */
#if ADVANCED_MODE
static void load_configuration(void)
{
	char path[MAX_PATH];
	char *p, *q, *r;
	int n, size;
	FILE *fp;
	HANDLE module;
	LRESULT range;

	module = GetModuleHandle(NULL);
	GetModuleFileNameA(module, path, MAX_PATH);
	p = strrchr(path, '\\');
	if (p == NULL)
		return;
	*p = '\0';
	SetCurrentDirectoryA(path);
	
	size = MAX_PATH - (int)(p - path) - 1;
	strncat_s(p, size, "\\conf.txt", size);
	p[size] = '\0';

	fopen_s(&fp, path, "r");
	if (fp == NULL)
		return;
	fseek(fp, 0, SEEK_END);
	size = ftell(fp);
	fseek(fp, 0, SEEK_SET);
	p = (char *)malloc(size + 1);
	if (p != NULL)
	{
		size = (int)fread(p, 1, size, fp);
		p[size] = '\0';

		q = strchr(p, '\n');
		if (q != NULL)
		{
			*q = '\0';
			q++;

			r = strchr(q, '\n');
			if (r != NULL)
			{
				*r = '\0';
				n = atoi(r + 1); // 正数表示勾选了, 负数表示没有勾选, 绝对值等于延时的毫秒数
				if (n > 0)
				{
					Button_SetCheck(hwndCheck[1], TRUE);
					EnableWindow(hwndEdit[2], TRUE);
					InvalidateRect(hwndSpin, NULL, FALSE);
				}
				else if (n < 0)
					n = -n;

				range = SendMessage(hwndSpin, UDM_GETRANGE, 0, 0);
				if (n >= HIWORD(range) && n <= LOWORD(range))
					SendMessage(hwndSpin, UDM_SETPOS, 0, n);
			}
			SetWindowTextA(hwndEdit[1], q);
		}

		if (_strnicmp(p, "com", 3) == 0)
			defport = atoi(p + 3);
		free(p);
	}
	fclose(fp);
	conf_changed = FALSE;
}
#endif

#if ADVANCED_MODE
/* 复位并运行固件 */
static void reset_and_run(int reset)
{
	int len;
	LPTSTR cmdfmt;

	if (!downloading)
	{
		len = Edit_GetTextLength(hwndEdit[1]) + 2;
		cmdfmt = (LPTSTR)malloc(len * sizeof(TCHAR));
		if (cmdfmt != NULL)
		{
			cmdfmt[0] = (reset) ? 'Y' : 'N';
			Edit_GetText(hwndEdit[1], cmdfmt + 1, len - 1);
			downloading = 1;
			szMessage[0] = '\0';
			enable_controls(FALSE);
			hThread = CreateThread(NULL, 0, reset_and_run_process, cmdfmt, 0, NULL);
			if (hThread == NULL)
				free(cmdfmt);
		}
	}
}

/* 发送复位命令的线程 */
static DWORD CALLBACK reset_and_run_process(LPVOID lpParameter)
{
	int err = 0;
	int i, len, port;
	uint32_t data;
	BOOL ret;
	COMMTIMEOUTS timeouts = {0};
	HANDLE hPort;
	LPTSTR cmdfmt = (LPTSTR)lpParameter + 1;
	LPTSTR cmd;
	PROCESS_INFORMATION pi = {0};
	STARTUPINFO si = {0};

	CoInitializeEx(NULL, COINIT_MULTITHREADED);
	len = lstrlen(cmdfmt) + 50;
	cmd = (LPTSTR)malloc(len * sizeof(TCHAR));
	if (cmd != NULL)
	{
		set_progress(-1);
		port = get_port();
		if (*(cmdfmt - 1) == 'Y')
		{
			// 复位
			hPort = open_port(port);
			if (hPort != INVALID_HANDLE_VALUE)
			{
				timeouts.WriteTotalTimeoutConstant = 10000;
				SetCommTimeouts(hPort, &timeouts);
				
				data = 0xabababab;
				wait_port(); // 先等待
				i = write_port(hPort, &data, 4); // 后复位
				if (i != 4)
				{
					err = 1;
					lstrcpy(szMessage, TEXT("复位失败!"));
				}
				close_port(hPort); // 复位后马上关串口, 准备启动串口调试工具
				wait_port();
			}
			else
				err = 1;
		}

		if (!err)
		{
			_sntprintf_s(cmd, len, len, cmdfmt, port);
			for (i = 0; cmd[i] != '\0'; i++)
			{
				if (cmd[i] != ' ' && cmd[i] != '\t')
					break;
			}

			if (cmd[i] != '\0') // 命令不为空
			{
				si.cb = sizeof(STARTUPINFO);
				ret = CreateProcess(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
				if (!ret)
					lstrcpy(szMessage, TEXT("启动串口调试工具失败!\n指定的串口调试工具可能未安装。"));
			}
		}
		free(cmd);
	}

	free(lpParameter);
	if (hwndDlg != NULL)
		PostMessage(hwndDlg, WM_USER, 2, 0);
	CoUninitialize();
	return 0;
}

/* 保存配置 */
static void save_configuration(void)
{
	char *str;
	int i, n, len;
	FILE *fp;
	HWND list[2] = {hwndCombo, hwndEdit[1]};

	fopen_s(&fp, "conf.txt", "w");
	if (fp != NULL)
	{
		for (i = 0; i < _countof(list); i++)
		{
			len = GetWindowTextLengthA(list[i]) + 1;
			str = (char *)malloc(len);
			if (str != NULL)
			{
				GetWindowTextA(list[i], str, len);
				fprintf(fp, "%s\n", str);
				free(str);
			}
			else
				fprintf(fp, "\n");
		}

		n = (int)SendMessage(hwndSpin, UDM_GETPOS, 0, 0);
		if (!Button_GetCheck(hwndCheck[1]))
			n = -n;
		fprintf(fp, "%d\n", n);

		fclose(fp);
		conf_changed = FALSE;
	}
}
#endif

/* 设置进度条进度 */
static void set_progress(int value)
{
	FLASHWINFO flash;
	LONG_PTR style;
	TCHAR str[20];

	style = GetWindowLongPtr(hwndProgress, GWL_STYLE);
	if (value == -1)
	{
		// 循环滚动
		if ((style & PBS_MARQUEE) == 0)
		{
			SetWindowLongPtr(hwndProgress, GWL_STYLE, style | PBS_MARQUEE);
			SendMessage(hwndProgress, PBM_SETMARQUEE, 1, 0);
		}
		Taskbar_SetProgress(hwndDlg, TBPF_INDETERMINATE, 0, 0);
	}
	else
	{
		if (style & PBS_MARQUEE)
		{
			SendMessage(hwndProgress, PBM_SETMARQUEE, 0, 0);
			SetWindowLongPtr(hwndProgress, GWL_STYLE, style & ~PBS_MARQUEE);
		}

		if (value >= 0 && value <= 100)
		{
			// 正常进度
			SendMessage(hwndProgress, PBM_SETPOS, value, 0);
			Taskbar_SetProgress(hwndDlg, TBPF_NORMAL, value, 100);
		}
		else if (value == 101)
		{
			// 完成
			value = 100;
			SendMessage(hwndProgress, PBM_SETPOS, 100, 0);

			Taskbar_SetProgress(hwndDlg, TBPF_NORMAL, 100, 100);
			flash.cbSize = sizeof(FLASHWINFO);
			flash.dwFlags = FLASHW_TIMERNOFG | FLASHW_TRAY; // 一直闪烁到用户切换回窗口
			flash.dwTimeout = 0;
			flash.hwnd = hwndDlg;
			flash.uCount = 3;
			FlashWindowEx(&flash); // 任务栏图标闪烁 (黄色)
		}
		else if (value == 102)
		{
			// 清除任务栏进度
			Taskbar_SetProgress(hwndDlg, TBPF_NOPROGRESS, 0, 0);
		}
		else if (value == -2)
		{
			// 错误
			Taskbar_SetProgress(hwndDlg, TBPF_ERROR, 1, 1);
		}
	}

	if (value == 0)
		SetDlgItemText(hwndDlg, IDC_STATIC5, TEXT("已检测到设备"));
	else if (value > 0 && value <= 100)
	{
		_stprintf_s(str, _countof(str), TEXT("进度: %d%%"), value);
		SetDlgItemText(hwndDlg, IDC_STATIC5, str);
	}
}

/* 显示固件文件信息 */
static void update_fileinfo(const HexFile *hexfile)
{
	TCHAR str[50];

	if (hexfile == NULL)
	{
		SetDlgItemText(hwndDlg, IDC_STATIC2, TEXT(""));
		SetDlgItemText(hwndDlg, IDC_STATIC3, TEXT(""));
		SetDlgItemText(hwndDlg, IDC_STATIC4, TEXT(""));
	}
	else
	{
		if (hexfile->size < 1024)
			_stprintf_s(str, _countof(str), TEXT("%uB"), hexfile->size);
		else if (hexfile->size < 1048576)
			_stprintf_s(str, _countof(str), TEXT("%.2fKB (%u字节)"), hexfile->size / 1024.0f, hexfile->size);
		else
			_stprintf_s(str, _countof(str), TEXT("%.2fMB (%u字节)"), hexfile->size / 1048576.0f, hexfile->size);
		SetDlgItemText(hwndDlg, IDC_STATIC2, str);

		_stprintf_s(str, _countof(str), TEXT("0x%08x - 0x%08x"), hexfile->start_addr, hexfile->end_addr - 1);
		SetDlgItemText(hwndDlg, IDC_STATIC3, str);

		_stprintf_s(str, _countof(str), TEXT("0x%08x"), hexfile->entry_point);
		SetDlgItemText(hwndDlg, IDC_STATIC4, str);
	}
	EnableWindow(hwndButton[0], hexfile != NULL);
}

/* 对话框消息处理 */
static INT_PTR CALLBACK DlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	int wmId, wmEvent;
	PNMLINK pnmLink;

	switch (uMsg)
	{
	case WM_COMMAND:
		wmId = LOWORD(wParam);
		wmEvent = HIWORD(wParam);
		switch (wmId)
		{
		case IDC_BUTTON1:
			browse_files();
			break;
		case IDC_BUTTON2:
			load_commports();
			break;
		case IDC_BUTTON4:
			DialogBox(hinstMain, MAKEINTRESOURCE(IDD_DIALOG3), hDlg, Bluetooth_DlgProc);
			break;
		case IDC_CHECK2:
			EnableWindow(hwndEdit[2], Button_GetCheck(hwndCheck[1]));
			InvalidateRect(hwndSpin, NULL, FALSE); // 重绘数值调整按扭 (兼容XP系统)
#if ADVANCED_MODE
			conf_changed = TRUE;
#endif
			break;
#if ADVANCED_MODE
		case IDC_BUTTON3:
			reset_and_run(1);
			break;
		case IDC_BUTTON5:
			download_firmware(HEADER_CRC_CONFIG);
			break;
		case IDC_BUTTON6:
			reset_and_run(0);
			break;
		case IDC_COMBO1:
			if (wmEvent == CBN_EDITCHANGE || wmEvent == CBN_SELCHANGE)
				conf_changed = TRUE;
			break;
		case IDC_EDIT2:
		case IDC_EDIT3:
			if (wmEvent == EN_CHANGE)
				conf_changed = TRUE;
			break;
#endif
		case IDOK:
			download_firmware(HEADER_FIRMWARE_INFO);
			break;
		case IDCANCEL:
			// 点击了对话框关闭按钮
			if (confirm_cancel())
			{
#if ADVANCED_MODE
				if (conf_changed)
					save_configuration();
#endif
				hwndDlg = NULL;
				EndDialog(hDlg, 0);
			}
			break;
		}
		break;
	case WM_INITDIALOG:
		hwndDlg = hDlg;
		hwndButton[0] = GetDlgItem(hDlg, IDOK);
		hwndButton[1] = GetDlgItem(hDlg, IDC_BUTTON1);
		hwndButton[2] = GetDlgItem(hDlg, IDC_BUTTON2);
		hwndButton[4] = GetDlgItem(hDlg, IDC_BUTTON4);
		hwndCheck[1] = GetDlgItem(hDlg, IDC_CHECK2);
		hwndCombo = GetDlgItem(hDlg, IDC_COMBO1);
		hwndEdit[0] = GetDlgItem(hDlg, IDC_EDIT1);
		hwndEdit[2] = GetDlgItem(hDlg, IDC_EDIT3);
		hwndProgress = GetDlgItem(hDlg, IDC_PROGRESS1);
		hwndSpin = GetDlgItem(hDlg, IDC_SPIN1);
		SendMessage(hwndSpin, UDM_SETRANGE, 0, MAKELPARAM(30000, 1));
		SendMessage(hwndSpin, UDM_SETPOS, 0, 2000); // 默认延时2秒
#if ADVANCED_MODE
		hwndButton[3] = GetDlgItem(hDlg, IDC_BUTTON3);
		hwndButton[5] = GetDlgItem(hDlg, IDC_BUTTON5);
		hwndButton[6] = GetDlgItem(hDlg, IDC_BUTTON6);
		hwndCheck[0] = GetDlgItem(hDlg, IDC_CHECK1);
		Button_SetCheck(hwndCheck[0], TRUE);
		hwndEdit[1] = GetDlgItem(hDlg, IDC_EDIT2);
#endif

		lParam = (LPARAM)LoadImage(hinstMain, MAKEINTRESOURCE(IDI_ICON1), IMAGE_ICON, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR);
		SendMessage(hDlg, WM_SETICON, ICON_BIG, lParam);
		lParam = (LPARAM)LoadImage(hinstMain, MAKEINTRESOURCE(IDI_ICON1), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR);
		SendMessage(hDlg, WM_SETICON, ICON_SMALL, lParam);

		SendMessage(hwndProgress, PBM_SETRANGE, 0, MAKELPARAM(1, 100));
		SetDlgItemText(hDlg, IDC_STATIC5, DEFAULT_TEXT);
		update_fileinfo(NULL);
#if ADVANCED_MODE
		load_configuration();
#endif
		load_commports();
		break;
	case WM_NOTIFY:
		pnmLink = (PNMLINK)lParam;
		if (pnmLink->hdr.code == NM_CLICK && pnmLink->hdr.idFrom == IDC_SYSLINK1)
			MessageBox(hDlg, SAMPLE_CODE, TEXT("示例代码"), 0);
		break;
	case WM_USER:
		/* 串口线程即将退出时要执行的代码 */
		if (wParam >= 1 && wParam <= 2)
		{
			switch (wParam)
			{
			case 1:
				/* 下载完毕 */
				if (downloading)
				{
					if (szMessage[0] != '\0')
						set_progress(-2);
					else
						set_progress(101);
				}

				switch (lParam)
				{
				case HEADER_FIRMWARE_INFO:
					if (downloading)
					{
						if (szMessage[0] == '\0')
							MessageBox(hDlg, TEXT("固件下载完毕!"), TEXT("下载固件"), MB_ICONINFORMATION);
						else
							MessageBox(hDlg, szMessage, TEXT("下载固件"), MB_ICONWARNING);
					}
					Button_SetText(hwndButton[0], TEXT("下载"));
					break;
#if ADVANCED_MODE
				case HEADER_CRC_CONFIG:
					if (downloading)
					{
						if (szMessage[0] == '\0')
							MessageBox(hDlg, TEXT("修改CRC配置成功!"), TEXT("CRC配置"), MB_ICONINFORMATION);
						else
							MessageBox(hDlg, szMessage, TEXT("CRC配置"), MB_ICONWARNING);
					}
					Button_SetText(hwndButton[5], TEXT("配置CRC校验"));
					break;
#endif
				}
				SetDlgItemText(hDlg, IDC_STATIC5, DEFAULT_TEXT);
				break;
			case 2:
				/* 复位并运行固件完毕 */
				if (szMessage[0] != '\0')
				{
					set_progress(-2);
					MessageBox(hDlg, szMessage, TEXT("运行"), MB_ICONWARNING);
				}
				break;
			}

			downloading = 0;
			enable_controls(TRUE);
			set_progress(102);
		}
		break;
	}
	return 0;
}

/* 主函数 */
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
	LRESULT ret;

	hinstMain = hInstance;
	InitCommonControls();
	CoInitializeEx(NULL, COINIT_MULTITHREADED);

#if ADVANCED_MODE
	ret = DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DlgProc);
#else
	ret = DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG2), NULL, DlgProc);
#endif

	if (hThread != NULL)
	{
		downloading = 0;
		WaitForSingleObject(hThread, INFINITE);
	}
	CoUninitialize();
	return (int)ret;
}

bluetooth.c:

#include 
#include 
#include 
#include 
#include 
#include 
#include "bluetooth.h"
#include "encoding.h"
#include "port.h"
#include "resource.h"

#pragma comment(lib, "uxtheme.lib")

static int add_device(const BluetoothDevice *device);
static DWORD CALLBACK bluetooth_process(LPVOID lpParameter);
static void copy_hc05_addr(char *dest, int destsize, const char *src);
static void dlg_init(void);
static void enable_controls(BOOL enabled);
static void start_bluetooth_process(uintptr_t opcode);

extern HINSTANCE hinstMain;
extern TCHAR szMessage[500];
static int searching_devices, testing_baudrate;
static int thread_active;
static HANDLE hThread;
static HIMAGELIST hImageList;
static HWND hwndButton[4], hwndDlg, hwndEdit[2], hwndList;

static int add_device(const BluetoothDevice *device)
{
	char buf[100];
	wchar_t wbuf[100];
	LVITEMW lvi;

	lvi.mask = LVIF_IMAGE | LVIF_PARAM | LVIF_TEXT; // 设置图标、自定义参数和文本
	if (device != NULL)
	{
		if (device->name[0] == '\0')
			_snprintf_s(buf, sizeof(buf), sizeof(buf), "%s", device->addr); // 只显示蓝牙地址
		else
			_snprintf_s(buf, sizeof(buf), sizeof(buf), "%s (%s)", device->name, device->addr); // 显示蓝牙名称和地址
		utf8to16_wbuf(buf, wbuf, sizeof(wbuf)); // UTF8转UTF16
		lvi.iImage = 0; // 蓝牙图标
	}
	else
	{
		wcscpy_s(wbuf, _countof(wbuf), L"空设备 (用于回到从机模式)");
		lvi.iImage = 1; // 无图标
	}
	lvi.iItem = 0; // 暂时先插入到开头, 等待自动排序 (控件已启用自动排序)
	lvi.iSubItem = 0; // 第一列
	lvi.lParam = (device != NULL) ? device->param : -1; // 设备序号 (连接时使用)
	lvi.pszText = wbuf;
	lvi.iItem = (int)SendMessage(hwndList, LVM_INSERTITEMW, 0, (LPARAM)&lvi); // 返回值为排序后的行号

	if (device != NULL && device->rssi_valid)
	{
		_snwprintf_s(wbuf, _countof(wbuf), _countof(wbuf), L"%d", device->rssi);
		lvi.mask = LVIF_TEXT; // 只设置文本
		lvi.iSubItem = 1; // 第二列
		lvi.pszText = wbuf;
		SendMessage(hwndList, LVM_SETITEMW, 0, (LPARAM)&lvi);
	}
	return lvi.iItem;
}

static DWORD CALLBACK bluetooth_process(LPVOID lpParameter)
{
	const int baudrate_list[] = {38400, 115200, 57600, 19200, 9600, 4800, 2400, 0}; // HC-05蓝牙模块AT模式下的波特率固定为38400, 所以要放第一个
	char addr[20];
	char buffer[200];
	char *p, *q;
	int baudrate[3] = {0}; // 三个下标分别表示波特率、停止位和校验位
	int i, j, n;
	int msgtype = 1;
	wchar_t *ws;
	uintptr_t opcode = (uintptr_t)lpParameter;
	BluetoothDevice device;
	BOOL ishc05 = FALSE, hc05_baudok = FALSE;
	COMMTIMEOUTS timeouts = {0};
	DCB dcb;
	HANDLE hPort;
	LVITEMA lvi;

	SetDlgItemText(hwndDlg, IDC_STATIC2, TEXT("正在打开串口..."));
	hPort = open_port(-1); // 打开串口, 若出错则会将错误消息填入szMessage
	if (hPort == INVALID_HANDLE_VALUE)
	{
		msgtype = 2;
		goto end;
	}
	dcb.DCBlength = sizeof(DCB);
	GetCommState(hPort, &dcb);

	/* 波特率匹配 */
	SetDlgItemText(hwndDlg, IDC_STATIC2, TEXT("正在匹配波特率..."));
	timeouts.ReadTotalTimeoutConstant = 500;
	timeouts.WriteTotalTimeoutConstant = 1000; // 蓝牙串口必须设置发送超时时间, 避免因蓝牙设备不存在而无限等待
	SetCommTimeouts(hPort, &timeouts);
	testing_baudrate = 1;
	for (i = 0; baudrate_list[i] != 0; i++)
	{
		if (dcb.BaudRate != baudrate_list[i])
		{
			dcb.BaudRate = baudrate_list[i];
			SetCommState(hPort, &dcb);
		}
		for (j = 0; j < 2; j++)
		{
			if (!testing_baudrate)
			{
				lstrcpy(szMessage, TEXT("操作已取消"));
				goto end;
			}
			write_port_str(hPort, "AT\r\n");
			read_port_line(hPort, buffer, sizeof(buffer), 200);
			if (strcmp(buffer, "OK") == 0)
				break;
		}
		if (j != 2)
			break;
	}
	testing_baudrate = 0;
	baudrate[0] = baudrate_list[i];
	if (baudrate[0] == 0)
	{
		// 找不到合适的波特率, 或者已有主机连上使得AT指令不可用
		msgtype = 2;
		lstrcpy(szMessage, TEXT("蓝牙串口不能响应AT指令!\n若使用的是HC-05模块,必须在按住KEY按键的情况下通电,否则无法进入AT模式。\n波特率多次匹配失败后,HC-05模块有可能死机,可尝试模块重新上电。\n若使用的是CC2541模块,则可能是因为蓝牙已连接上设备。\n若想要更换设备连接,请拔下蓝牙串口设备再重新插上。"));
		goto end;
	}
	else if (baudrate[0] == 38400)
	{
		// 判断是否为HC-05模块
		read_port_str(hPort, buffer, sizeof(buffer)); // 吸收掉所有未读完的数据
		write_port_str(hPort, "AT+UART?\r\n");
		read_port_line(hPort, buffer, sizeof(buffer), 0);
		if (strncmp(buffer, "+UART:", 6) == 0)
		{
			n = sscanf_s(buffer, "+UART:%d,%d,%d", &baudrate[0], &baudrate[1], &baudrate[2]);
			hc05_baudok = (n == 3 && baudrate[0] == 115200 && baudrate[1] == 0 && baudrate[2] == 0);
			read_port_line(hPort, buffer, sizeof(buffer), 0);
			ishc05 = (strcmp(buffer, "OK") == 0);
		}
	}

	/* 配置波特率 */
	if ((!ishc05 && baudrate[0] != 115200) || (ishc05 && !hc05_baudok))
	{
		if (!ishc05)
			_sntprintf_s(szMessage, _countof(szMessage), _countof(szMessage), TEXT("当前蓝牙串口的波特率为%d。\n是否需要修改为115200?"), baudrate[0]);
		else
			_sntprintf_s(szMessage, _countof(szMessage), _countof(szMessage), TEXT("当前蓝牙串口的通信模式的波特率配置为“%d,%d,%d”。\n是否需要修改为“115200,0,0”?\n\n注意: HC-05模块的AT模式波特率固定为38400。\n这里指的是通信模式下,也就是不按住KEY按键给模块上电时的波特率。"), baudrate[0], baudrate[1], baudrate[2]);

		if (MessageBox(hwndDlg, szMessage, TEXT("蓝牙串口"), MB_ICONQUESTION | MB_OKCANCEL) == IDOK)
			szMessage[0] = '\0';
		else
		{
			lstrcpy(szMessage, TEXT("串口波特率不正确"));
			goto end;
		}
		
		if (!ishc05)
		{
			write_port_str(hPort, "AT+BAUD8\r\n");
			read_port_line(hPort, buffer, sizeof(buffer), 0);
			if (strcmp(buffer, "+BAUD=8") == 0 || strcmp(buffer, "+BAUD=115200") == 0)
			{
				read_port_line(hPort, buffer, sizeof(buffer), 0);
				if (strcmp(buffer, "OK") == 0)
				{
					dcb.BaudRate = CBR_115200;
					SetCommState(hPort, &dcb);
				}
			}
			if (dcb.BaudRate != CBR_115200)
			{
				lstrcpy(szMessage, TEXT("配置串口波特率失败"));
				goto end;
			}

			for (i = 0; i < 10; i++)
			{
				write_port_str(hPort, "AT+BAUD\r\n");
				read_port_line(hPort, buffer, sizeof(buffer), 200);
				if (strcmp(buffer, "+BAUD=8") == 0)
					break;
			}
			if (i == 10)
			{
				lstrcpy(szMessage, TEXT("串口波特率配置成功但没有生效"));
				goto end;
			}
		}
		else
		{
			write_port_str(hPort, "AT+UART=115200,0,0\r\n");
			read_port_line(hPort, buffer, sizeof(buffer), 0);
			if (strcmp(buffer, "OK") != 0)
			{
				lstrcpy(szMessage, TEXT("配置串口波特率失败"));
				goto end;
			}
		}
	}

	/* 执行命令 */
	timeouts.ReadTotalTimeoutConstant = (!ishc05) ? 25 : 300;
	SetCommTimeouts(hPort, &timeouts);
	switch (opcode)
	{
	case 1:
		/* 搜索设备 */
		SetDlgItemText(hwndDlg, IDC_STATIC2, TEXT("正在搜索设备..."));
		if (!ishc05)
		{
			i = 10;
			write_port_str(hPort, "AT+ROLE0\r\n");
			read_port_line(hPort, buffer, sizeof(buffer), 0);
			if (strcmp(buffer, "+ROLE=0") == 0)
			{
				read_port_line(hPort, buffer, sizeof(buffer), 0);
				if (strcmp(buffer, "OK") == 0)
				{
					// 等待生效
					for (i = 0; i < 10; i++)
					{
						write_port_str(hPort, "AT+ROLE\r\n");
						read_port_line(hPort, buffer, sizeof(buffer), 200);
						Sleep(500); // 延时, 以免命令没反应 (时间一定要够, 不然后面获取蓝牙名称会为空)
						if (strcmp(buffer, "+ROLE=0") == 0)
							break;
					}
				}
			}
			if (i == 10)
			{
				lstrcpy(szMessage, TEXT("配置串口从模式失败"));
				break;
			}

			// 经测试, 某些CC2541蓝牙模块只有在从模式下才能获取到蓝牙名称和地址
			write_port_str(hPort, "AT+NAME\r\n");
			read_port_line(hPort, buffer, sizeof(buffer), 200); // 最后一个参数不能是0, 以免下一行的命令字符串被设置成设备的蓝牙名称, 结果蓝牙名称以\r\n开头
			if (strncmp(buffer, "+NAME=", 6) == 0)
			{
				ws = utf8to16(buffer + 6);
				if (ws != NULL)
				{
					SetWindowTextW(hwndEdit[0], ws);
					free(ws);
				}
			}

			write_port_str(hPort, "AT+LADDR\r\n");
			read_port_line(hPort, buffer, sizeof(buffer), 0);
			if (strncmp(buffer, "+LADDR=", 7) == 0)
				SetDlgItemTextA(hwndDlg, IDC_STATIC4, buffer + 7);

			write_port_str(hPort, "AT+PIN\r\n");
			read_port_line(hPort, buffer, sizeof(buffer), 0);
			if (strncmp(buffer, "+PIN=", 5) == 0)
				SetWindowTextA(hwndEdit[1], buffer + 5);
		
			// 搜索前需要先切换到主模式
			i = 10;
			write_port_str(hPort, "AT+ROLE1\r\n");
			read_port_line(hPort, buffer, sizeof(buffer), 0);
			if (strcmp(buffer, "+ROLE=1") == 0)
			{
				read_port_line(hPort, buffer, sizeof(buffer), 0);
				if (strcmp(buffer, "OK") == 0)
				{
					// 等待生效
					for (i = 0; i < 10; i++)
					{
						write_port_str(hPort, "AT+ROLE\r\n");
						read_port_line(hPort, buffer, sizeof(buffer), 200);
						Sleep(500); // 延时, 以免命令没反应 (时间也一定要够, 不然后面无法响应搜索命令)
						if (strcmp(buffer, "+ROLE=1") == 0)
							break;
					}
				}
			}
			if (i == 10)
			{
				lstrcpy(szMessage, TEXT("配置串口主模式失败"));
				break;
			}
			
			// 开始搜索
			write_port_str(hPort, "AT+INQ\r\n");
			searching_devices = 1;
			while (read_port_line_cancellable(hPort, buffer, sizeof(buffer), 15000, &searching_devices))
			{
				if (strncmp(buffer, "+INQ:", 5) == 0)
				{
					p = strchr(buffer, ' ');
					if (p == NULL)
						p = strchr(buffer, '=');
					if (p != NULL)
					{
						*p = '\0';
						memset(&device, 0, sizeof(device));
						device.param = atoi(buffer + 5);

						p += 3;
						for (i = 0; i < 6; i++)
						{
							device.addr[3 * i] = p[2 * i];
							device.addr[3 * i + 1] = p[2 * i + 1];
							device.addr[3 * i + 2] = (i == 5) ? '\0' : ':';
						}

						if (p[12] == ' ')
						{
							p += 13;
							q = strchr(p, ' ');
							if (q != NULL)
								*q = '\0';
							device.rssi_valid = TRUE;
							device.rssi = atoi(p);
						}
						add_device(&device);
					}
				}
				else if (strcmp(buffer, "+INQE") == 0)
				{
					read_port_line(hPort, buffer, sizeof(buffer), 250); // 有可能会收到“Devices Found”
					break;
				}
			}
		}
		else
		{
			write_port_str(hPort, "AT+NAME?\r\n");
			read_port_line(hPort, buffer, sizeof(buffer), 0);
			if (strncmp(buffer, "+NAME:", 6) == 0)
			{
				ws = utf8to16(buffer + 6);
				if (ws != NULL)
				{
					SetWindowTextW(hwndEdit[0], ws);
					free(ws);
				}
			}
			read_port_line(hPort, buffer, sizeof(buffer), 0); // 接收"OK"这一行

			write_port_str(hPort, "AT+ADDR?\r\n");
			read_port_line(hPort, buffer, sizeof(buffer), 0);
			if (strncmp(buffer, "+ADDR:", 6) == 0)
			{
				copy_hc05_addr(addr, sizeof(addr), buffer + 6);
				SetDlgItemTextA(hwndDlg, IDC_STATIC4, addr);
			}
			read_port_line(hPort, buffer, sizeof(buffer), 0);

			write_port_str(hPort, "AT+PSWD?\r\n");
			read_port_line(hPort, buffer, sizeof(buffer), 0);
			if (strncmp(buffer, "+PSWD:", 6) == 0)
				SetWindowTextA(hwndEdit[1], buffer + 6);
			read_port_line(hPort, buffer, sizeof(buffer), 0);

			// 开始搜索
			write_port_str(hPort, "AT+INQ\r\n");
			searching_devices = 1;
			while (read_port_line_cancellable(hPort, buffer, sizeof(buffer), 15000, &searching_devices))
			{
				if (strncmp(buffer, "+INQ:", 5) == 0)
				{
					memset(&device, 0, sizeof(device));
					copy_hc05_addr(device.addr, sizeof(device.addr), buffer + 5);
					p = strchr(buffer, ',');
					if (p != NULL)
					{
						p = strchr(p + 1, ',');
						if (p != NULL)
						{
							p++;
							q = strchr(p, ',');
							if (q != NULL)
							{
								*q = '\0';
								strncpy_s(device.name, sizeof(device.name), q + 1, sizeof(device.name) - 1);
							}
							device.rssi_valid = TRUE;
							sscanf_s(p, "%x", &device.rssi);
							device.rssi = (short)device.rssi;
							add_device(&device);
						}
					}
				}
				else if (strcmp(buffer, "OK") == 0)
					break;
				else if (strncmp(buffer, "ERROR", 5) == 0)
				{
					searching_devices = 0;
					lstrcpy(szMessage, TEXT("设备搜索失败"));
					goto end;
				}
			}

			// 停止搜索
			write_port_str(hPort, "AT+INQC\r\n");
			read_port_str(hPort, buffer, sizeof(buffer));
		}
		if (searching_devices)
		{
			searching_devices = 0;
			lstrcpy(szMessage, TEXT("设备搜索完毕"));
		}
		else
			lstrcpy(szMessage, TEXT("操作已取消"));
		break;
	case 2:
		/* 连接设备 */
		SetDlgItemText(hwndDlg, IDC_STATIC2, TEXT("正在连接设备..."));
		lvi.mask = LVIF_PARAM | LVIF_TEXT; // 获取参数和文本
		lvi.iItem = ListView_GetNextItem(hwndList, -1, LVNI_SELECTED); // 选中行的行号
		lvi.iSubItem = 0;
		if (!ishc05)
		{
			lvi.pszText = addr + 2; // 存放文本的缓冲区
			lvi.cchTextMax = sizeof(addr) - 2; // 缓冲区的大小
			SendMessage(hwndList, LVM_GETITEMA, 0, (LPARAM)&lvi);

			if (lvi.lParam != -1)
			{
				// 在主机模式下连接设备
				addr[0] = '0';
				addr[1] = 'x';
				for (i = 1; i < 6; i++)
				{
					addr[2 * i + 2] = addr[3 * i + 2];
					addr[2 * i + 3] = addr[3 * i + 3];
				}
				addr[14] = '\0';

				_snprintf_s(buffer, sizeof(buffer), sizeof(buffer), "AT+CONN%d\r\n", (int)lvi.lParam);
				write_port_str(hPort, buffer);
				read_port_line(hPort, buffer, sizeof(buffer), 0);
				if (strncmp(buffer, "+Connecting", 11) == 0 && strcmp(buffer + 13, addr) == 0)
				{
					read_port_line(hPort, buffer, sizeof(buffer), 10000);
					if (strncmp(buffer, "+Connected", 10) == 0 && strcmp(buffer + 12, addr) == 0)
					{
						msgtype = 4;
						lstrcpy(szMessage, TEXT("连接设备成功!"));
						break;
					}
				}
				else if (strcmp(buffer, "OK") == 0)
				{
					msgtype = 4;
					lstrcpy(szMessage, TEXT("模块已开始连接设备,请打开串口查看是否连接成功。\n若设备不在附近,模块会一直尝试连接,直到设备出现在附近并连接成功。"));
					break;
				}
			}
			else
			{
				// 回到从机模式
				write_port_str(hPort, "AT+ROLE0\r\n");
				read_port_line(hPort, buffer, sizeof(buffer), 0);
				if (strcmp(buffer, "+ROLE=0") == 0)
				{
					read_port_line(hPort, buffer, sizeof(buffer), 0);
					if (strcmp(buffer, "OK") == 0)
					{
						msgtype = 4;
						lstrcpy(szMessage, TEXT("已配置为从机模式。"));
						break;
					}
				}

				lstrcpy(szMessage, TEXT("配置串口从模式失败"));
				break;
			}
		}
		else
		{
			lvi.pszText = buffer;
			lvi.cchTextMax = sizeof(buffer);
			SendMessage(hwndList, LVM_GETITEMA, 0, (LPARAM)&lvi);

			if (lvi.lParam == 0)
			{
				// 设为主机模式
				n = (int)strlen(buffer);
				if (buffer[n - 1] == ')')
				{
					buffer[n - 1] = '\0';
					p = &buffer[n - 18];
				}
				else
					p = buffer;
				addr[0] = p[0];
				addr[1] = p[1];
				addr[2] = p[3];
				addr[3] = p[4];
				addr[4] = ',';
				addr[5] = p[6];
				addr[6] = p[7];
				addr[7] = ',';
				addr[8] = p[9];
				addr[9] = p[10];
				addr[10] = p[12];
				addr[11] = p[13];
				addr[12] = p[15];
				addr[13] = p[16];
				addr[14] = '\0';

				write_port_str(hPort, "AT+ROLE=1\r\n");
				read_port_line(hPort, buffer, sizeof(buffer), 0);
				if (strcmp(buffer, "OK") == 0)
				{
					write_port_str(hPort, "AT+CMODE=0\r\n");
					read_port_line(hPort, buffer, sizeof(buffer), 0);
					if (strcmp(buffer, "OK") == 0)
					{
						_snprintf_s(buffer, sizeof(buffer), sizeof(buffer), "AT+BIND=%s\r\n", addr);
						write_port_str(hPort, buffer);
						read_port_line(hPort, buffer, sizeof(buffer), 0);
						if (strcmp(buffer, "OK") == 0)
						{
							msgtype = 4;
							lstrcpy(szMessage, TEXT("已配置好设备连接请求。\n请在松开KEY键的情况下给蓝牙模块重新上电,模块将自动连接所选设备并通信。通信时请使用115200波特率。\n若所选设备为另一个HC-05蓝牙模块,那么这个蓝牙模块的配对密码必须要和当前蓝牙模块相同,才能连接成功。\n若所选设备为电脑,且蓝牙模块没有和此电脑配对,则蓝牙模块连接电脑时,电脑会自动提示配对新蓝牙设备并要求输入配对密码。\n\n请注意:HC-05蓝牙模块和电脑配对后,会产生两个COM口。\n一个是传入(Incoming),另一个是传出(Outgoing)。\n无论蓝牙模块是否在电脑附近,始终都能成功打开传入COM口。\n但只有当蓝牙模块在电脑附近时,才能成功打开传出COM口。\n由于这是蓝牙模块主动连接电脑,所以电脑上应该打开传入COM口。"));
							break;
						}
					}
				}
			}
			else
			{
				// 回到从机模式
				write_port_str(hPort, "AT+ROLE=0\r\n");
				read_port_line(hPort, buffer, sizeof(buffer), 0);
				if (strcmp(buffer, "OK") == 0)
				{
					msgtype = 4;
					lstrcpy(szMessage, TEXT("已配置为从机模式。\n请在松开KEY键的情况下给蓝牙模块重新上电,然后在电脑上打开传出COM口,即可连接蓝牙模块。通信时请使用115200波特率。"));
					break;
				}
			}
		}
		// 若连接不上, 则只有Connecting没有Connected, 设备还能继续响应AT指令
		msgtype = 2;
		lstrcpy(szMessage, TEXT("连接设备失败!"));
		break;
	case 3:
		/* 重命名 */
		SetDlgItemText(hwndDlg, IDC_STATIC2, TEXT("正在修改设备名称..."));
		if (!ishc05)
		{
			p = gettext_utf8(hwndEdit[0]);
			if (p != NULL)
			{
				_snprintf_s(buffer, sizeof(buffer), sizeof(buffer), "AT+NAME%s\r\n", p);
				free(p);

				write_port_str(hPort, buffer);
				read_port_line(hPort, buffer, sizeof(buffer), 0);
				if (strncmp(buffer, "+NAME=", 6) == 0)
					read_port_line(hPort, buffer, sizeof(buffer), 0);
			}

			if (p == NULL || strcmp(buffer, "OK") != 0)
			{
				lstrcpy(szMessage, TEXT("设备名称修改失败"));
				break;
			}

			// 复位后生效
			write_port_str(hPort, "AT+RESET\r\n");
			read_port_line(hPort, buffer, sizeof(buffer), 0);
			if (strcmp(buffer, "+RESET") == 0)
				read_port_line(hPort, buffer, sizeof(buffer), 0);
			if (strcmp(buffer, "OK") != 0)
			{
				msgtype = 3;
				lstrcpy(szMessage, TEXT("设备名称修改成功,但需要重启模块后才能生效。"));
				break;
			}
		}
		else
		{
			p = gettext_utf8(hwndEdit[0]);
			if (p != NULL)
			{
				_snprintf_s(buffer, sizeof(buffer), sizeof(buffer), "AT+NAME=%s\r\n", p);
				free(p);

				write_port_str(hPort, buffer);
				read_port_line(hPort, buffer, sizeof(buffer), 0);
			}

			if (p == NULL || strcmp(buffer, "OK") != 0)
			{
				lstrcpy(szMessage, TEXT("设备名称修改失败"));
				break;
			}

			// HC-05不需要复位, 立即就能生效
		}
		lstrcpy(szMessage, TEXT("设备名称修改成功"));
		break;
	case 4:
		/* 设置密码 */
		SetDlgItemText(hwndDlg, IDC_STATIC2, TEXT("正在修改配对密码..."));
		if (!ishc05)
		{
			p = gettext_utf8(hwndEdit[1]);
			if (p != NULL)
			{
				_snprintf_s(buffer, sizeof(buffer), sizeof(buffer), "AT+PIN%s\r\n", p);
				free(p);

				write_port_str(hPort, buffer);
				read_port_line(hPort, buffer, sizeof(buffer), 0);
				if (strncmp(buffer, "+PIN=", 5) == 0)
					read_port_line(hPort, buffer, sizeof(buffer), 0);
			}

			if (p == NULL || strcmp(buffer, "OK") != 0)
			{
				lstrcpy(szMessage, TEXT("设备配对密码修改失败"));
				break;
			}

			// 复位后生效
			write_port_str(hPort, "AT+RESET\r\n");
			read_port_line(hPort, buffer, sizeof(buffer), 0);
			if (strcmp(buffer, "+RESET") == 0)
				read_port_line(hPort, buffer, sizeof(buffer), 0);
			if (strcmp(buffer, "OK") != 0)
			{
				msgtype = 3;
				lstrcpy(szMessage, TEXT("设备配对密码修改成功,但需要重启模块后才能生效。"));
				break;
			}
		}
		else
		{
			p = gettext_utf8(hwndEdit[1]);
			if (p != NULL)
			{
				_snprintf_s(buffer, sizeof(buffer), sizeof(buffer), "AT+PSWD=%s\r\n", p);
				free(p);

				write_port_str(hPort, buffer);
				read_port_line(hPort, buffer, sizeof(buffer), 0);
			}

			if (p == NULL || strcmp(buffer, "OK") != 0)
			{
				lstrcpy(szMessage, TEXT("设备配对密码修改失败"));
				break;
			}
		}
		lstrcpy(szMessage, TEXT("设备配对密码修改成功"));
		break;
	}

end:
	close_port(hPort);
	PostMessage(hwndDlg, WM_USER, opcode, msgtype);
	return 0;
}

static void copy_hc05_addr(char *dest, int destsize, const char *src)
{
	int values[3] = {0};

	sscanf_s(src, "%x:%x:%x", &values[0], &values[1], &values[2]);
	_snprintf_s(dest, destsize, destsize, "%02X:%02X:%02X:%02X:%02X:%02X",
		(values[0] >> 8) & 0xff, values[0] & 0xff,
		values[1] & 0xff,
		(values[2] >> 16) & 0xff, (values[2] >> 8) & 0xff, values[2] & 0xff
	);
}

static void dlg_init(void)
{
	int cx, cy;
	HICON hIcon;
	LVCOLUMN lvc;

	SetDlgItemText(hwndDlg, IDC_STATIC2, TEXT(""));
	SetDlgItemText(hwndDlg, IDC_STATIC4, TEXT(""));

	hwndButton[0] = GetDlgItem(hwndDlg, IDOK);
	hwndButton[1] = GetDlgItem(hwndDlg, IDC_BUTTON1);
	hwndButton[2] = GetDlgItem(hwndDlg, IDC_BUTTON2);
	hwndButton[3] = GetDlgItem(hwndDlg, IDC_BUTTON3);
	hwndEdit[0] = GetDlgItem(hwndDlg, IDC_EDIT1);
	hwndEdit[1] = GetDlgItem(hwndDlg, IDC_EDIT2);

	hwndList = GetDlgItem(hwndDlg, IDC_LIST1);
	ListView_SetExtendedListViewStyle(hwndList, LVS_EX_DOUBLEBUFFER);
	
	// 启用Vista风格列表控件
	// 请注意: 调用此函数设置主题成功后, 一定要响应ListView的NM_KILLFOCUS消息, 调用InvalidateRect重绘控件
	// 不然窗口失去焦点后, 选择框文字部分是灰色的, 但图标部分仍然是蓝色的
	SetWindowTheme(hwndList, L"Explorer", NULL);

	cx = GetSystemMetrics(SM_CXSMICON);
	cy = GetSystemMetrics(SM_CYSMICON);
	hImageList = ImageList_Create(cx, cy, ILC_COLOR32, 1, 1);
	hIcon = LoadImage(hinstMain, MAKEINTRESOURCE(IDI_ICON2), IMAGE_ICON, cx, cy, LR_DEFAULTCOLOR);
	ImageList_AddIcon(hImageList, hIcon);
	ListView_SetImageList(hwndList, hImageList, LVSIL_SMALL);

	lvc.mask = LVCF_TEXT | LVCF_WIDTH;
	lvc.cx = 250;
	lvc.pszText = TEXT("设备地址");
	ListView_InsertColumn(hwndList, 0, &lvc);
	lvc.cx = 100;
	lvc.pszText = TEXT("信号强度");
	ListView_InsertColumn(hwndList, 1, &lvc);
	add_device(NULL);
}

static void enable_controls(BOOL enabled)
{
	EnableWindow(hwndButton[0], enabled);
	EnableWindow(hwndButton[1], enabled);
	EnableWindow(hwndButton[2], enabled);
	EnableWindow(hwndButton[3], (enabled && ListView_GetSelectedCount(hwndList) != 0));
	Edit_SetReadOnly(hwndEdit[0], !enabled);
	Edit_SetReadOnly(hwndEdit[1], !enabled);
}

static void start_bluetooth_process(uintptr_t opcode)
{
	if (!thread_active)
	{
		thread_active = 1;
		szMessage[0] = '\0';
		enable_controls(FALSE);
		if (opcode == 1)
		{
			ListView_DeleteAllItems(hwndList);
			add_device(NULL);
		}
		hThread = CreateThread(NULL, 0, bluetooth_process, (LPVOID)opcode, 0, NULL);
	}
}

INT_PTR CALLBACK Bluetooth_DlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	int wmId;
	LPCTSTR title = NULL;
	LPNMHDR lpnmhdr;
	LPNMITEMACTIVATE lpnmia;

	switch (uMsg)
	{
	case WM_COMMAND:
		wmId = LOWORD(wParam);
		switch (wmId)
		{
		case IDOK:
			start_bluetooth_process(1);
			break;
		case IDCANCEL:
			if (thread_active)
			{
				// 请求停止线程里面的耗时操作
				searching_devices = 0;
				testing_baudrate = 0;
			}
			else
				EndDialog(hDlg, 0);
			break;
		case IDC_BUTTON1:
			if (Edit_GetTextLength(hwndEdit[0]) != 0)
				start_bluetooth_process(3);
			else
			{
				MessageBox(hDlg, TEXT("设备名称不能为空!"), TEXT("重命名设备"), MB_ICONWARNING);
				SetFocus(hwndEdit[0]);
			}
			break;
		case IDC_BUTTON2:
			if (Edit_GetTextLength(hwndEdit[1]) != 0)
				start_bluetooth_process(4);
			else
			{
				MessageBox(hDlg, TEXT("设备配对密码不能为空!"), TEXT("设置密码"), MB_ICONWARNING);
				SetFocus(hwndEdit[1]);
			}
			break;
		case IDC_BUTTON3:
			if (ListView_GetSelectedCount(hwndList) != 0)
				start_bluetooth_process(2);
			break;
		}
		break;
	case WM_INITDIALOG:
		hwndDlg = hDlg;
		dlg_init();
		break;
	case WM_NOTIFY:
		lpnmhdr = (LPNMHDR)lParam;
		if (lpnmhdr->idFrom == IDC_LIST1)
		{
			switch (lpnmhdr->code)
			{
			case LVN_ITEMACTIVATE:
			case NM_CLICK:
			case NM_RCLICK:
				lpnmia = (LPNMITEMACTIVATE)lpnmhdr;
				if (lpnmia->iItem != -1 && !thread_active)
				{
					EnableWindow(hwndButton[3], TRUE);

					// 若双击了项目, 则开始连接
					if (lpnmhdr->code == LVN_ITEMACTIVATE)
						start_bluetooth_process(2);
				}
				else
					EnableWindow(hwndButton[3], FALSE);
				break;
			case LVN_KEYDOWN:
				PostMessage(hDlg, WM_USER, 100, 0);
				break;
			case NM_KILLFOCUS:
				InvalidateRect(lpnmhdr->hwndFrom, NULL, FALSE);
				break;
			}
		}
		break;
	case WM_USER:
		if (wParam >= 1 && wParam <= 4)
		{
			switch (wParam)
			{
			case 1:
				title = TEXT("搜索设备");
				break;
			case 2:
				title = TEXT("连接设备");
				break;
			case 3:
				title = TEXT("重命名设备");
				break;
			case 4:
				title = TEXT("设置密码");
				break;
			}

			switch (lParam)
			{
			case 1:
				// 改变提示文字
				SetDlgItemText(hDlg, IDC_STATIC2, szMessage);
				break;
			case 2:
				// 弹出警告窗口
				MessageBox(hDlg, szMessage, title, MB_ICONWARNING);
				SetDlgItemText(hDlg, IDC_STATIC2, TEXT(""));
				break;
			case 3:
				// 弹出信息窗口
				MessageBox(hDlg, szMessage, title, MB_ICONINFORMATION);
				SetDlgItemText(hDlg, IDC_STATIC2, TEXT(""));
				break;
			case 4:
				// 弹出信息窗口并关闭对话框
				MessageBox(hDlg, szMessage, title, MB_ICONINFORMATION);
				EndDialog(hDlg, 0);
				break;
			}
			thread_active = 0;
			enable_controls(TRUE);
		}
		else if (wParam == 100)
			EnableWindow(hwndButton[3], ListView_GetSelectedCount(hwndList) != 0 && !thread_active);
		break;
	}
	return 0;
}

encoding.c:

#include 
#include "encoding.h"

/* 获取文本框输入的内容, 并转换成UTF8编码 */
char *gettext_utf8(HWND hWnd)
{
	char *s = NULL;
	int n;
	wchar_t *ws;

	n = GetWindowTextLengthW(hWnd) + 1;
	ws = malloc(n * sizeof(wchar_t));
	if (ws != NULL)
	{
		GetWindowTextW(hWnd, ws, n);
		s = utf16to8(ws);
		free(ws);
	}
	return s;
}

/* UTF8转UTF16 */
wchar_t *utf8to16(char *s)
{
	wchar_t *ws;
	int n;

	n = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0);
	ws = malloc(n * sizeof(wchar_t));
	if (ws != NULL)
		MultiByteToWideChar(CP_UTF8, 0, s, -1, ws, n);
	return ws;
}

int utf8to16_wbuf(char *s, wchar_t *wbuf, int bufsize)
{
	return MultiByteToWideChar(CP_UTF8, 0, s, -1, wbuf, bufsize / sizeof(wchar_t));
}

/* UTF16转UTF8 */
char *utf16to8(wchar_t *ws)
{
	char *s;
	int n;

	n = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL);
	s = malloc(n);
	if (s != NULL)
		WideCharToMultiByte(CP_UTF8, 0, ws, -1, s, n, NULL, NULL);
	return s;
}

你可能感兴趣的:(STM32,Win32,单片机,蓝牙,Win32,c语言)