feat(bluetooth): 添加多按钮支持和WiFi连接音频反馈

添加MultiButton库支持多按键功能,重构SPI显示屏驱动代码,
迁移MP3音频文件至正确目录并集成WiFi连接状态音频提示音。

- 添加Multi_Button.c源文件和相关头文件包含
- 重构spi_st7735s.c中的数组初始化格式,优化代码可读性
- 将MP3音频文件从Development_Docs/MP3迁移到Core/Bsp/BSP_Device/bsp_mp3/MP3
- 在WiFi连接过程中添加MP3音频反馈(连接成功/失败提示音)
- 优化ST7735显示屏驱动中的DMA传输模式支持
```
This commit is contained in:
2026-02-23 16:59:34 +08:00
parent ce8d6fd2eb
commit 9cadad138e
37 changed files with 980 additions and 201 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,81 +0,0 @@
# MP3语音播放集成总结
## 语音文件列表
| 编号 | 文件名 | 语音内容 | 触发场景 |
|------|--------|----------|----------|
| 001 | 001.mp3 | 智能宠物喂食系统启动完成,进入待机模式 | 系统上电完成 |
| 002 | 002.mp3 | WiFi连接成功云平台数据同步已开启 | WiFi连接成功 |
| 003 | 003.mp3 | WiFi连接失败请检查网络配置 | WiFi连接失败 |
| 004 | 004.mp3 | 自动喂食模式启动,步进电机开始出粮 | 自动喂食启动 |
| 005 | 005.mp3 | 正在出粮,称重模块实时监测中 | 喂食进行中 |
| 006 | 006.mp3 | 喂食完成,当前食物重量已达设定值 | 喂食完成 |
| 007 | 007.mp3 | 手动喂食指令已接收,开始出粮 | 手动喂食触发 |
| 008 | 008.mp3 | 警告:食物余量低于下限,请及时添加 | 食物余量不足 |
| 009 | 009.mp3 | 水位低于阈值,水泵启动,开始自动补水 | 自动补水启动 |
| 010 | 010.mp3 | 补水完成,水位已达设定上限 | 补水完成 |
| 011 | 011.mp3 | 警告:水位过低,请检查水源或水泵 | 水位过低 |
| 012 | 012.mp3 | 检测到宠物靠近,水位偏低,启动自动补水 | PIR联动补水 |
| 013 | 013.mp3 | 已切换至自动运行模式 | 切换自动模式 |
| 014 | 014.mp3 | 已切换至手动控制模式 | 切换手动模式 |
| 015 | 015.mp3 | 参数设置已保存,系统配置已更新 | 参数设置成功 |
| 016 | 016.mp3 | 自动喂食时间已更新 | 定时时间修改 |
| 017 | 017.mp3 | 喂食重量阈值已更新 | 重量阈值修改 |
| 018 | 018.mp3 | 接收到微信小程序远程控制指令 | 远程指令接收 |
| 019 | 019.mp3 | 数据上传失败,请检查网络连接 | 数据上传失败 |
| 020 | 020.mp3 | 系统检测到异常,请检查硬件模块 | 系统异常 |
**说明:**
- 编号从001开始顺序排列
- 文件名与编号对应001.mp3、002.mp3...
- 每项包含:编号、文件名、语音内容、触发场景四列信息
- 共20条语音文件覆盖系统所有核心功能模块
## MP3驱动API
### 初始化函数
```c
HAL_StatusTypeDef MP3_Init(void);
```
- 初始化MP3模块
- 设置音量30选择TF卡音源
### 播放控制
```c
HAL_StatusTypeDef MP3_Play(uint16_t index); // 播放指定曲目 (1-9999)
HAL_StatusTypeDef MP3_Stop(void); // 停止播放
HAL_StatusTypeDef MP3_Pause(void); // 暂停播放
```
### 参数设置
```c
HAL_StatusTypeDef MP3_SetVolume(uint8_t volume); // 设置音量 (0-30)
HAL_StatusTypeDef MP3_SetSource(uint8_t source); // 设置音源 (1=U盘, 2=SD卡)
```
## MP3命令格式
10字节固定格式:
```
字节0: 0x7E 帧头
字节1: 0xFF 版本号
字节2: 0x06 数据长度
字节3: 命令码 (0x06=音量, 0x09=音源, 0x12=播放索引)
字节4: 0x00 反馈
字节5: 数据高字节
字节6: 数据低字节
字节7: 校验和高位
字节8: 校验和低位
字节9: 0xEF 帧尾
```
校验和算法: `0x10000 - sum(字节1~6)`
## 注意事项
1. 确保TF卡中音频文件命名为001.mp3, 002.mp3, ... 格式
2. MP3模块需等待2秒上电稳定
3. USART3波特率设置为9600
4. 首次播放建议测试`MP3_Play(1)`是否正常工作
5. 所有语音播放函数已集成到相应按键处理逻辑中

View File

@@ -1,209 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MP3播放函数使用示例
展示如何在代码中使用封装好的MP3播放函数
"""
# ==========================================
# C代码使用示例
# ==========================================
example_code = """
// 在 freertos.c 中使用 MP3 播放函数
#include "mp3_driver.h"
// ==========================================
// 1. 初始化MP3模块 (在 MP3 任务中)
// ==========================================
void MP3(void *argument) {
// 等待模块上电稳定
osDelay(2000);
// 初始化MP3模块
HAL_StatusTypeDef ret = MP3_Init();
if (ret != HAL_OK) {
elog_e("MP3", "MP3模块初始化失败");
return;
}
// ... 其他代码
}
// ==========================================
// 2. 播放指定曲目
// ==========================================
// 在 Sensor 任务中播放语音
MP3_Play(1); // 播放第1首: "欢迎使用智能按摩器"
MP3_Play(2); // 播放第2首: "定时10分钟"
MP3_Play(3); // 播放第3首: "定时20分钟"
// ... 依此类推
// ==========================================
// 3. 其他常用函数
// ==========================================
MP3_Stop(); // 停止播放
MP3_Pause(); // 暂停播放
MP3_SetVolume(20); // 设置音量 (0-30)
MP3_SetSource(MP3_SOURCE_SD_CARD); // 选择TF卡音源
// ==========================================
// 4. 实际应用示例:时间设定
// ==========================================
if (timer_minutes == 10) {
MP3_Play(2); // "定时10分钟"
} else if (timer_minutes == 20) {
MP3_Play(3); // "定时20分钟"
} else if (timer_minutes == 30) {
MP3_Play(4); // "定时30分钟"
} else if (timer_minutes == 0) {
MP3_Play(5); // "定时已取消"
}
// ==========================================
// 5. 实际应用示例:档位控制
// ==========================================
if (current_gear == 1) {
MP3_Play(8); // "一档"
} else if (current_gear == 2) {
MP3_Play(9); // "二档"
} else if (current_gear == 3) {
MP3_Play(10); // "三档"
}
// ==========================================
// 6. 实际应用示例:加热控制
// ==========================================
if (hot_state) {
MP3_Play(15); // "加热已开启"
} else {
MP3_Play(16); // "加热已关闭"
}
// ==========================================
// 7. 实际应用示例:定时结束
// ==========================================
if (remaining_seconds == 0) {
MP3_Play(18); // "定时结束,按摩结束"
Motor_SetGear(0); // 停止电机
// ... 其他清理工作
}
"""
# ==========================================
# 语音文件映射表
# ==========================================
voice_mapping = """
+------+--------------------------+
| 编号 | 语音内容 |
+------+--------------------------+
| 001 | 欢迎使用智能按摩器 |
| 002 | 定时10分钟 |
| 003 | 定时20分钟 |
| 004 | 定时30分钟 |
| 005 | 定时已取消 |
| 006 | 按摩已停止 |
| 007 | 按摩开始 |
| 008 | 一档 |
| 009 | 二档 |
| 010 | 三档 |
| 011 | 已到最大档位 (可选) |
| 012 | 一档 (降档用) |
| 013 | 二档 (降档用) |
| 014 | 按摩停止 |
| 015 | 加热已开启 |
| 016 | 加热已关闭 |
| 017 | 请先设定时间 |
| 018 | 定时结束,按摩结束 |
+------+--------------------------+
"""
# ==========================================
# MP3命令格式说明
# ==========================================
command_format = """
MP3命令格式 (10字节):
====================================================
字节 | 内容 | 说明
-----+---------------+---------------------------
0 | 0x7E | 帧头 (固定)
1 | 0xFF | 版本号 (固定)
2 | 0x06 | 数据长度 (固定)
3 | 命令码 | 0x06=音量, 0x09=音源, 0x12=播放索引
4 | 0x00 | 反馈 (固定)
5 | 数据高字节 | 根据命令不同
6 | 数据低字节 | 根据命令不同
7 | 校验和高位 | 计算得出
8 | 校验和低位 | 计算得出
9 | 0xEF | 帧尾 (固定)
====================================================
校验和计算方法:
1. 从索引1 (0xFF) 开始累加到索引6 (数据低字节)
2. 用 0x10000 减去累加结果
3. 高位在前,低位在后
示例 (播放文件1):
数据: 0xFF 0x06 0x12 0x00 0x00 0x01
求和: 0xFF + 0x06 + 0x12 + 0x00 + 0x00 + 0x01 = 0x0118
校验: 0x10000 - 0x0118 = 0xFEE8
高位: 0xFE, 低位: 0xE8
完整命令: 0x7E 0xFF 0x06 0x12 0x00 0x00 0x01 0xFE 0xE8 0xEF
"""
if __name__ == "__main__":
print("=" * 70)
print("MP3播放函数使用示例".center(70))
print("=" * 70)
print("\n" + "=" * 70)
print("1. C代码使用示例")
print("=" * 70)
print(example_code)
print("\n" + "=" * 70)
print("2. 语音文件映射表")
print("=" * 70)
print(voice_mapping)
print("\n" + "=" * 70)
print("3. MP3命令格式说明")
print("=" * 70)
print(command_format)
print("\n" + "=" * 70)
print("4. API 函数列表")
print("=" * 70)
print("""
HAL_StatusTypeDef MP3_Init(void);
- 初始化MP3模块
- 返回: HAL_OK(成功) 或 HAL_ERROR(失败)
HAL_StatusTypeDef MP3_Play(uint16_t index);
- 播放指定曲目 (索引范围: 1-9999)
- 参数: index - 曲目编号
- 返回: HAL_OK(成功) 或 HAL_ERROR(失败)
HAL_StatusTypeDef MP3_Stop(void);
- 停止当前播放
- 返回: HAL_OK(成功) 或 HAL_ERROR(失败)
HAL_StatusTypeDef MP3_Pause(void);
- 暂停当前播放
- 返回: HAL_OK(成功) 或 HAL_ERROR(失败)
HAL_StatusTypeDef MP3_SetVolume(uint8_t volume);
- 设置音量 (范围: 0-30)
- 参数: volume - 音量值
- 返回: HAL_OK(成功) 或 HAL_ERROR(失败)
HAL_StatusTypeDef MP3_SetSource(uint8_t source);
- 设置音源
- 参数: source - MP3_SOURCE_U_DISK(1) 或 MP3_SOURCE_SD_CARD(2)
- 返回: HAL_OK(成功) 或 HAL_ERROR(失败)
""")
print("=" * 70)

View File

@@ -1,222 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
音频文件按时间戳排序并重命名工具
功能:
1. 查找指定路径下的MP3文件
2. 按文件创建时间从先到后排序
3. 显示排序后的文件列表
4. 用户可指定起始编号(如0001或0002)
5. 重命名文件为0001.mp3, 0002.mp3等格式
"""
import os
import sys
import glob
from pathlib import Path
def find_mp3_files(directory):
"""查找指定目录下的所有MP3文件"""
mp3_files = glob.glob(os.path.join(directory, "*.mp3"))
return mp3_files
def is_timestamp_filename(filename):
"""判断文件名是否为时间戳格式(纯数字.mp3)"""
# 去除扩展名
name_without_ext = os.path.splitext(filename)[0]
# 检查是否为纯数字且至少为10位(时间戳)
return name_without_ext.isdigit() and len(name_without_ext) >= 10
def is_numbered_filename(filename):
"""判断文件名是否为编号格式(如0001.mp3)"""
# 去除扩展名
name_without_ext = os.path.splitext(filename)[0]
# 检查是否为纯数字长度为3-4位(编号格式)
return name_without_ext.isdigit() and len(name_without_ext) >= 3 and len(name_without_ext) <= 4
def filter_timestamp_files(files):
"""筛选出时间戳格式的MP3文件跳过编号格式的文件"""
timestamp_files = []
skipped_files = []
for file_path in files:
filename = os.path.basename(file_path)
if is_numbered_filename(filename):
skipped_files.append(filename)
elif is_timestamp_filename(filename):
timestamp_files.append(file_path)
else:
skipped_files.append(filename)
return timestamp_files, skipped_files
def sort_files_by_time(files):
"""按文件创建时间从先到后排序"""
# 获取文件信息并按创建时间排序
file_info = []
for file_path in files:
stat = os.stat(file_path)
file_info.append({
'path': file_path,
'name': os.path.basename(file_path),
'ctime': stat.st_ctime # 创建时间
})
# 按创建时间升序排序
sorted_files = sorted(file_info, key=lambda x: x['ctime'])
return sorted_files
def display_file_list(sorted_files, skipped_files):
"""显示排序后的文件列表"""
print("\n" + "=" * 80)
if skipped_files:
print(f"已跳过 {len(skipped_files)} 个非时间戳格式的文件:")
for filename in skipped_files:
print(f" 跳过: {filename}")
print()
print(f"找到 {len(sorted_files)} 个时间戳格式的MP3文件按创建时间排序:")
print("=" * 80)
for i, file_info in enumerate(sorted_files, 1):
import time
time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info['ctime']))
print(f"{i:3d}. {file_info['name']:<50} 创建时间: {time_str}")
print("=" * 80)
def get_start_number():
"""获取用户指定的起始编号"""
while True:
try:
user_input = input("\n请输入起始编号(如1或2将转换为0001/0002格式): ").strip()
if not user_input:
print("输入不能为空,请重新输入!")
continue
start_num = int(user_input)
if start_num < 1 or start_num > 9999:
print("编号必须在1-9999之间请重新输入!")
continue
return start_num
except ValueError:
print("请输入有效的数字!")
def rename_files(sorted_files, start_number):
"""重命名文件为编号格式"""
print("\n开始重命名文件...")
print("=" * 80)
success_count = 0
error_count = 0
for i, file_info in enumerate(sorted_files, start_number):
old_path = file_info['path']
directory = os.path.dirname(old_path)
new_name = f"{i:04d}.mp3"
new_path = os.path.join(directory, new_name)
try:
os.rename(old_path, new_path)
print(f"{file_info['name']} -> {new_name}")
success_count += 1
except Exception as e:
print(f"{file_info['name']} 重命名失败: {e}")
error_count += 1
print("=" * 80)
print(f"重命名完成! 成功: {success_count}, 失败: {error_count}")
def confirm_operation():
"""确认是否执行重命名操作"""
while True:
user_input = input("\n确认要重命名这些文件吗? (y/n): ").strip().lower()
if user_input in ['y', 'yes']:
return True
elif user_input in ['n', 'no']:
return False
else:
print("请输入 y 或 n")
def main():
print("=" * 80)
print("音频文件按时间戳排序并重命名工具".center(80))
print("=" * 80)
# 获取目标目录
while True:
directory = input("\n请输入要处理的MP3文件所在路径: ").strip()
directory = directory.strip('"').strip("'") # 去除可能的引号
if not directory:
print("路径不能为空!")
continue
if not os.path.isdir(directory):
print(f"路径不存在: {directory}")
continue
break
# 查找MP3文件
mp3_files = find_mp3_files(directory)
if not mp3_files:
print(f"\n在路径 {directory} 中未找到MP3文件!")
return
# 筛选时间戳格式文件,跳过编号格式文件
timestamp_files, skipped_files = filter_timestamp_files(mp3_files)
if not timestamp_files:
print(f"\n在路径 {directory} 中未找到时间戳格式的MP3文件!")
if skipped_files:
print(f"但找到 {len(skipped_files)} 个已编号格式的文件:")
for filename in skipped_files:
print(f" {filename}")
return
# 按时间排序
sorted_files = sort_files_by_time(timestamp_files)
# 显示文件列表
display_file_list(sorted_files, skipped_files)
# 获取起始编号
start_number = get_start_number()
# 预览重命名结果
print("\n重命名预览:")
print("-" * 80)
for i, file_info in enumerate(sorted_files, start_number):
new_name = f"{i:04d}.mp3"
print(f"{i:4d}. {file_info['name']} -> {new_name}")
print("-" * 80)
# 确认操作
if confirm_operation():
rename_files(sorted_files, start_number)
else:
print("\n操作已取消!")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\n程序被用户中断!")
sys.exit(0)
except Exception as e:
print(f"\n程序执行出错: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@@ -1,118 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MP3命令校验和验证工具
验证校验和算法是否正确
"""
def calc_checksum(cmd):
"""
计算MP3命令校验和
算法:从版本号(索引1)开始到数据结束所有字节相加后取反再加1
返回:高位和低位
"""
sum_val = 0
# 从版本号(索引1)开始累加,到数据结束(索引6)
for i in range(1, 7):
sum_val += cmd[i]
# 取反加1 (即0x10000 - sum_val)
sum_val = 0x10000 - sum_val
# 高位在前,低位在后
high = (sum_val >> 8) & 0xFF
low = sum_val & 0xFF
return high, low
def verify_command(cmd, name):
"""验证命令的校验和是否正确"""
print(f"\n{'='*60}")
print(f"验证命令: {name}")
print(f"{'='*60}")
# 提取原始数据
data = cmd[:7]
original_high = cmd[7]
original_low = cmd[8]
# 显示命令字节
print(f"命令字节: ", end="")
for i, byte in enumerate(cmd):
print(f"0x{byte:02X} ", end="")
if i == 3:
print("| ", end="")
print()
# 显示数据部分
print(f"数据部分 (索引1-6): ", end="")
for i in range(1, 7):
print(f"0x{cmd[i]:02X} ", end="")
if i == 3:
print("| ", end="")
print()
# 显示求和过程
sum_val = sum(cmd[1:7])
print(f"\n求和: {sum_val} (0x{sum_val:04X})")
print(f"取反加1: 0x10000 - 0x{sum_val:04X} = 0x{0x10000 - sum_val:04X}")
# 计算校验和
calc_high, calc_low = calc_checksum(cmd)
print(f"\n计算校验和: 高位=0x{calc_high:02X}, 低位=0x{calc_low:02X}")
print(f"原始校验和: 高位=0x{original_high:02X}, 低位=0x{original_low:02X}")
# 验证
if calc_high == original_high and calc_low == original_low:
print("[OK] 校验和验证通过!")
return True
else:
print("[FAIL] 校验和验证失败!")
return False
def main():
print("="*60)
print("MP3命令校验和验证工具")
print("="*60)
# 命令1开启声音 (音量30)
# 0x7E 0xFF 0x06 0x06 0x00 0x00 0x1E 0xFE 0xD7 0xEF
cmd1 = [0x7E, 0xFF, 0x06, 0x06, 0x00, 0x00, 0x1E, 0xFE, 0xD7, 0xEF]
verify_command(cmd1, "开启声音 (音量30)")
# 命令2选择TF卡
# 0x7E 0xFF 0x06 0x09 0x00 0x00 0x02 0xFE 0xF0 0xEF
cmd2 = [0x7E, 0xFF, 0x06, 0x09, 0x00, 0x00, 0x02, 0xFE, 0xF0, 0xEF]
verify_command(cmd2, "选择TF卡")
# 命令3播放文件1
# 0x7E 0xFF 0x06 0x12 0x00 0x00 0x01 0xFE 0xE8 0xEF
cmd3 = [0x7E, 0xFF, 0x06, 0x12, 0x00, 0x00, 0x01, 0xFE, 0xE8, 0xEF]
verify_command(cmd3, "播放文件1")
# 测试:生成其他曲目的播放命令
print(f"\n{'='*60}")
print("生成其他曲目的播放命令")
print(f"{'='*60}")
for index in [1, 10, 100, 9999]:
play_cmd = [0x7E, 0xFF, 0x06, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEF]
play_cmd[5] = (index >> 8) & 0xFF
play_cmd[6] = index & 0xFF
high, low = calc_checksum(play_cmd)
play_cmd[7] = high
play_cmd[8] = low
print(f"\n曲目 {index:4d}: ", end="")
for byte in play_cmd:
print(f"0x{byte:02X} ", end="")
print()
print(f"\n{'='*60}")
if __name__ == "__main__":
main()