Fix settings BT/restore icons and soft-off BLE protocol plumbing.

Point system-settings rows at the correct Flash icons, keep Main/UI alive across soft-off, and expose git build id for 01 0C with matching BLE tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yuquanjun 2026-09-09 07:03:52 +08:00
parent 57448b3da8
commit f1f9520637
12 changed files with 292 additions and 40 deletions

View File

@ -229,20 +229,23 @@ static void PowerOff(void)
LCD_FillByColor(0, 0, 240, 320, BLACK); LCD_FillByColor(0, 0, 240, 320, BLACK);
LCD_BLK_Clr(); LCD_BLK_Clr();
LCD_WR_REG(0x28); /* Display OFF */
app_tm1629_all_off(); app_tm1629_all_off();
app_tm1617_off(); app_tm1617_off();
/* 关闭电源硬件MCU 保持运行以便长按开机 / 关机充电画面 */ /* 关掉主电源/音源/功放/蓝牙MCU 仍跑以便长按开机与 Type-C 检测 */
BSP_MainPowerEnable(0); BSP_MainPowerEnable(0);
BSP_DreamCorePowerEnable(0); BSP_DreamCorePowerEnable(0);
BSP_HT7178PowerEnable(0); BSP_HT7178PowerEnable(0);
/* BT 最后关:给已排队的 UART4 ACK如 05 00留出模组转发窗口 */
rt_thread_mdelay(30);
BSP_BlueToothPowerEnable(0); BSP_BlueToothPowerEnable(0);
CurrUIProcress = IdleProcess; CurrUIProcress = IdleProcess;
MainTask_Sendmsg(MSG_ID_POWER_OFF, 0, 0, 0); MainTask_Sendmsg(MSG_ID_POWER_OFF, 0, 0, 0);
LOG_I("PWR", "power_off done (soft-off, no reset)"); LOG_I("PWR", "power_off done (soft-off)");
/* 关机时若已插着 Type-C立刻进充电画面 */ /* Type-C 有供电:维持现有逻辑,显示 Charging ImagePOWOFF_CHARG */
{ {
uint8_t i; uint8_t i;
for (i = 0; i < 4; i++) for (i = 0; i < 4; i++)
@ -256,7 +259,7 @@ static void PowerOff(void)
{ {
usb_charging_state = 1; usb_charging_state = 1;
usb_charging_laststate = 1; usb_charging_laststate = 1;
LOG_I("PWR", "soft-off with USB → charge UI"); LOG_I("PWR", "soft-off with Type-C → charge UI");
MainTask_Sendmsg(MSG_ID_POWOFF_CHARG, percentage, 1, 0); MainTask_Sendmsg(MSG_ID_POWOFF_CHARG, percentage, 1, 0);
} }
} }

View File

@ -179,8 +179,9 @@ void IdleProcess(DisplayTaskMessage_Type msg)
break; break;
case MSG_ID_POWER_OFF: case MSG_ID_POWER_OFF:
/* 关机:熄屏 + 关背光 */ /* 无 Type-C整机关机熄屏有 Type-C 时随后 POWOFF_CHARG 会再亮充电图 */
LCD_BLK_Clr(); LCD_BLK_Clr();
LCD_WR_REG(0x28); /* Display OFF */
LCD_FillByColor(0, 0, 240, 320, BLACK); LCD_FillByColor(0, 0, 240, 320, BLACK);
break; break;
@ -190,6 +191,8 @@ void IdleProcess(DisplayTaskMessage_Type msg)
case MSG_ID_TOUCH: case MSG_ID_TOUCH:
{ {
if (!powon)
break; /* 软关机已停触摸;兜底勿进模式选择 */
static uint32_t s_last_enter_ms; static uint32_t s_last_enter_ms;
uint32_t now = rt_tick_get() * 1000U / RT_TICK_PER_SECOND; uint32_t now = rt_tick_get() * 1000U / RT_TICK_PER_SECOND;
uint8_t hit = 0U; uint8_t hit = 0U;

View File

@ -1223,11 +1223,11 @@ void Draw_System_Item_List(const LEBEL_UI *UI, GUI_SWITCH *Group)
} }
break; break;
case GUI_BLUETOOTH_SW: case GUI_BLUETOOTH_SW:
/* 素材:系统设置图标-11白蓝牙符Flash 槽名 RESTORE尺寸 13x21 */ /* 素材:系统设置图标-11白蓝牙符→ ICON_SET_BLUETOOTH 13x21 */
LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12, LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12,
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_RESTORE_H) / 2), (uint16_t)(UI->y + (UI->h - UI0902_ICON_SET_BLUETOOTH_H) / 2),
UI0902_ICON_SYS_RESTORE_W, UI0902_ICON_SYS_RESTORE_H, UI0902_ICON_SET_BLUETOOTH_W, UI0902_ICON_SET_BLUETOOTH_H,
UI0902_ICON_SYS_RESTORE_ADDR); UI0902_ICON_SET_BLUETOOTH_ADDR);
UI_DrawTriLeft(val_x1, chev_cy); UI_DrawTriLeft(val_x1, chev_cy);
if(Group->Current) if(Group->Current)
LCD_ShowChinese_AutoAlign_Gradient(val_x1 + UI0902_TRI_L_W, val_x2 - UI0902_TRI_R_W, text_y,(const uint8_t*)"\xCA\xC7",LCD_ALIGN_CENTER, WHITE,c_top, c_bot,value_sz); LCD_ShowChinese_AutoAlign_Gradient(val_x1 + UI0902_TRI_L_W, val_x2 - UI0902_TRI_R_W, text_y,(const uint8_t*)"\xCA\xC7",LCD_ALIGN_CENTER, WHITE,c_top, c_bot,value_sz);
@ -1264,11 +1264,11 @@ void Draw_System_Item_List(const LEBEL_UI *UI, GUI_SWITCH *Group)
drv_nvm_save_to_flash(); drv_nvm_save_to_flash();
break; break;
case GUI_RESTORE: case GUI_RESTORE:
/* 逆时针箭头:当前在 VERSION 槽(19x18)RESTORE 槽曾误为蓝牙符 */ /* 素材:系统设置图标-13逆时针恢复→ ICON_SYS_RESTORE 19x18 */
LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12, LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12,
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_VERSION_H) / 2), (uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_RESTORE_H) / 2),
UI0902_ICON_SYS_VERSION_W, UI0902_ICON_SYS_VERSION_H, UI0902_ICON_SYS_RESTORE_W, UI0902_ICON_SYS_RESTORE_H,
UI0902_ICON_SYS_VERSION_ADDR); UI0902_ICON_SYS_RESTORE_ADDR);
/* 右三角与蓝牙/自动关机同一右缘val_x2 */ /* 右三角与蓝牙/自动关机同一右缘val_x2 */
UI_DrawTriRight((uint16_t)(val_x2 - UI0902_TRI_R_W), chev_cy); UI_DrawTriRight((uint16_t)(val_x2 - UI0902_TRI_R_W), chev_cy);
break; break;

View File

@ -663,7 +663,7 @@
<name>BUILDACTION</name> <name>BUILDACTION</name>
<archiveVersion>1</archiveVersion> <archiveVersion>1</archiveVersion>
<data> <data>
<prebuild></prebuild> <prebuild>python &quot;$PROJ_DIR$\..\..\tools\gen_git_user_fw_ver.py&quot;</prebuild>
<postbuild></postbuild> <postbuild></postbuild>
</data> </data>
</settings> </settings>
@ -1650,7 +1650,7 @@
<name>BUILDACTION</name> <name>BUILDACTION</name>
<archiveVersion>1</archiveVersion> <archiveVersion>1</archiveVersion>
<data> <data>
<prebuild></prebuild> <prebuild>python &quot;$PROJ_DIR$\..\..\tools\gen_git_user_fw_ver.py&quot;</prebuild>
<postbuild></postbuild> <postbuild></postbuild>
</data> </data>
</settings> </settings>

View File

@ -9,6 +9,9 @@
void USART2_SendData(uint8_t *data, uint16_t len); void USART2_SendData(uint8_t *data, uint16_t len);
void USART4_SendData(uint8_t *data, uint16_t len); void USART4_SendData(uint8_t *data, uint16_t len);
/* 等待 UART4 发送环形缓冲排空(供关机 ACK 等场景) */
void USART4_WaitTxIdle(uint32_t timeout_ms);
/* 接收出队:返回 1=取到数据0=队列空 */ /* 接收出队:返回 1=取到数据0=队列空 */
uint8_t USART2_RxPop(uint8_t *c); uint8_t USART2_RxPop(uint8_t *c);
uint8_t UART4_RxPop(uint8_t *c); uint8_t UART4_RxPop(uint8_t *c);

View File

@ -101,6 +101,23 @@ void USART4_SendData(uint8_t *data, uint16_t len)
uart_ring_send(&uart4, data, len); uart_ring_send(&uart4, data, len);
} }
void USART4_WaitTxIdle(uint32_t timeout_ms)
{
uint32_t t0 = rt_tick_get();
while ((rt_tick_get() - t0) < rt_tick_from_millisecond(timeout_ms ? timeout_ms : 1))
{
uint16_t h = uart4.tx_head;
uint16_t t = uart4.tx_tail;
if (IS_EMPTY(h, t))
{
/* <20><><EFBFBD><EFBFBD>պ<EFBFBD><D5BA>ٵ<EFBFBD><D9B5><EFBFBD>λ<EFBFBD>Ĵ<EFBFBD><C4B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> */
if (usart_flag_get(uart4.usart, USART_TDC_FLAG) == SET)
return;
}
rt_thread_mdelay(1);
}
}
/* USART2 ???????????? */ /* USART2 ???????????? */
uint8_t USART2_RxPop(uint8_t *c) uint8_t USART2_RxPop(uint8_t *c)
{ {

View File

@ -1,4 +1,5 @@
#include "includes.h" #include "includes.h"
#include "git_user_fw_ver.h"
// Э<><D0AD>֡ͷ/֡β // Э<><D0AD>֡ͷ/֡β
@ -162,12 +163,13 @@ static const BLE_SysExCmdItem bleSysExCmdTable[] =
*/ */
void processBLESysEXData(uint8_t* data, uint8_t cnt) void processBLESysEXData(uint8_t* data, uint8_t cnt)
{ {
//ResetAutoPowerCount();
// ֡У<D6A1><D0A3> // ֡У<D6A1><D0A3>
if (data[0] != FRAME_SYS_HEAD || data[cnt - 1] != FRAME_SYS_TAIL || cnt > UART4_PROCESS_BUFF_SIZE || data[1] != 0x60) if (data[0] != FRAME_SYS_HEAD || data[cnt - 1] != FRAME_SYS_TAIL || cnt > UART4_PROCESS_BUFF_SIZE || data[1] != 0x60)
{ {
return; return;
} }
/* App/BLE 活动视为用户操作,避免测协议时静置触发自动关机 */
ResetAutoPowerCount();
LOG_I("BLE", "sysex ok len=%u cmd=%02X %02X", (unsigned)cnt, data[2], data[3]); LOG_I("BLE", "sysex ok len=%u cmd=%02X %02X", (unsigned)cnt, data[2], data[3]);
// ȡ<><C8A1>ָ<EFBFBD><D6B8> + <20><>ָ<EFBFBD><D6B8> // ȡ<><C8A1>ָ<EFBFBD><D6B8> + <20><>ָ<EFBFBD><D6B8>
@ -584,10 +586,11 @@ static void BL_Sendmsg(uint16_t Data1, uint16_t Data2, uint16_t Data3, uint16_t
/*-------- 1. device info 0x01 --------*/ /*-------- 1. device info 0x01 --------*/
#define AT32_UID_BASE 0x1FFFF7E8UL /* 96-bit unique device ID */ #define AT32_UID_BASE 0x1FFFF7E8UL /* 96-bit unique device ID */
/* placeholders: 01 04/05/0C — bump manually at each release (Dream has no ver query) */ /* 01 04 音源版本YY.M.D = 音色资源日期Dream 无版本查询,发版时手改)
static const uint8_t SOUND_VER[4] = {0x4D, 0x06, 0x7F, 0x01}; * 26.9.8 2026-09-08Doc//0908 */
static const uint8_t UI_VER[4] = {0x21, 0x05, 0x7F, 0x01}; static const uint8_t SOUND_VER[4] = {26, 9, 8, 0x01};
static const uint8_t USER_FW_VER[4] = {0x4D, 0x06, 0x7F, 0x01}; /* 01 05 UI 资源/界面日期YY.M.D与 MCU 逻辑版本 Version[] 分开维护) */
static const uint8_t UI_VER[4] = {26, 9, 9, 0x01};
static void handleDevDisconnect(uint8_t *data) static void handleDevDisconnect(uint8_t *data)
{ {
@ -676,9 +679,16 @@ static void handleAutoPowerOff(uint8_t *data)
static void handleUserFwVer(uint8_t *data) static void handleUserFwVer(uint8_t *data)
{ {
uint8_t resp[9] = {0xF0, 0x60, 0x01, 0x0C, USER_FW_VER[0], USER_FW_VER[1], USER_FW_VER[2], USER_FW_VER[3], 0xF7}; /* 用户固件版本 = "{branch}_{short6}"[+ '*'],如 develop_0aedb4ASCIIBLE-MIDI 安全) */
uint8_t resp[4u + GIT_BUILD_ID_LEN + 1u];
(void)data; (void)data;
USART4_SendData(resp, sizeof(resp)); resp[0] = 0xF0;
resp[1] = 0x60;
resp[2] = 0x01;
resp[3] = 0x0C;
memcpy(&resp[4], GIT_BUILD_ID, GIT_BUILD_ID_LEN);
resp[4u + GIT_BUILD_ID_LEN] = 0xF7;
USART4_SendData(resp, (uint16_t)sizeof(resp));
} }
/* F0 60 01 11 <minutes> F7 : set auto power-off, 0 = disable */ /* F0 60 01 11 <minutes> F7 : set auto power-off, 0 = disable */
@ -827,6 +837,11 @@ static void handleDeviceReset(uint8_t *data)
{ {
uint8_t resp[6] = {0xF0, 0x60, 0x05, 0x00, 0x01, 0xF7}; uint8_t resp[6] = {0xF0, 0x60, 0x05, 0x00, 0x01, 0xF7};
(void)data; (void)data;
LOG_I("BLE", "05 00 ack then soft power off");
USART4_SendData(resp, sizeof(resp)); USART4_SendData(resp, sizeof(resp));
/* Soft-off cuts BT power immediately in PowerOff(); give UART4 + ATS2853
time to push the ACK Notify before requesting power-off. */
USART4_WaitTxIdle(50);
rt_thread_mdelay(120);
System_RequestPowerOff(); System_RequestPowerOff();
} }

View File

@ -0,0 +1,16 @@
/* Auto-generated by tools/gen_git_user_fw_ver.py — do not edit.
* git HEAD (dirty): 57448b3da8a3f912503e9d50313b5ec635888b69
* branch: develop develop
* 01 0C wire: develop_57448b*
*/
#ifndef GIT_USER_FW_VER_H
#define GIT_USER_FW_VER_H
#define GIT_COMMIT_ID_FULL "57448b3da8a3f912503e9d50313b5ec635888b69"
#define GIT_BRANCH_NAME "develop"
#define GIT_COMMIT_SHORT6 "57448b"
#define GIT_DIRTY (1)
#define GIT_BUILD_ID "develop_57448b*"
#define GIT_BUILD_ID_LEN 15u
#endif /* GIT_USER_FW_VER_H */

View File

@ -240,6 +240,35 @@ void StartTask(void)
/* 扫描任务可能已由 StartScanTask 拉起;重复 startup 会被 RT-Thread 忽略 */ /* 扫描任务可能已由 StartScanTask 拉起;重复 startup 会被 RT-Thread 忽略 */
StartScanTask(); StartScanTask();
/* Soft-off(StopFullTask) 会 detach 下列线程;开机须能重建,否则 UI/消息永久失效 */
if ((TaskMainThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
{
rt_thread_init(&TaskMainThread,
"TaskMainThread",
TaskMainThread_entry,
RT_NULL,
&TaskMainThread_Stack,
sizeof(TaskMainThread_Stack),
5,
20);
}
if ((TaskMainThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_INIT)
rt_thread_startup(&TaskMainThread);
if ((TaskUIThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
{
rt_thread_init(&TaskUIThread,
"TaskUIThread",
TaskUIThread_entry,
RT_NULL,
&TaskUIThread_Stack,
sizeof(TaskUIThread_Stack),
5,
20);
}
if ((TaskUIThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_INIT)
rt_thread_startup(&TaskUIThread);
if ((TaskBTHandleThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE) if ((TaskBTHandleThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
{ {
rt_thread_init(&TaskBTHandleThread, rt_thread_init(&TaskBTHandleThread,
@ -336,18 +365,12 @@ void StopFullTask(void)
LOG_I("TASK", "stop ScanThread"); LOG_I("TASK", "stop ScanThread");
rt_thread_detach(&TaskScanThread); rt_thread_detach(&TaskScanThread);
} }
if((TaskUIThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) { /* 软关机 = 系统关闭:停触摸/扫描/BT/伴奏。
LOG_I("TASK", "stop UIThread"); Main/UI Type-C Charging Image POWER_ON */
rt_thread_detach(&TaskUIThread);
}
if((TaskTouchThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) { if((TaskTouchThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
LOG_I("TASK", "stop TouchThread"); LOG_I("TASK", "stop TouchThread");
rt_thread_detach(&TaskTouchThread); rt_thread_detach(&TaskTouchThread);
} }
if((TaskMainThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
LOG_I("TASK", "stop MainThread");
rt_thread_detach(&TaskMainThread);
}
if((TaskBTHandleThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) { if((TaskBTHandleThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
LOG_I("TASK", "stop BTHandleThread"); LOG_I("TASK", "stop BTHandleThread");
rt_thread_detach(&TaskBTHandleThread); rt_thread_detach(&TaskBTHandleThread);
@ -356,8 +379,12 @@ void StopFullTask(void)
LOG_I("TASK", "stop BTRecvThread"); LOG_I("TASK", "stop BTRecvThread");
rt_thread_detach(&TaskBTRecvThread); rt_thread_detach(&TaskBTRecvThread);
} }
if((TaskAutobandThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
LOG_I("TASK", "stop AutobandThread");
rt_thread_detach(&TaskAutobandThread);
}
LOG_I("TASK", "StopFullTask done"); LOG_I("TASK", "StopFullTask done");
} }
static void OutTime_Stop_timer_callback(void *parameter) static void OutTime_Stop_timer_callback(void *parameter)

View File

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
"""Generate protocol/git_user_fw_ver.h from current git HEAD + branch.
用户固件版本 (01 0C) 线格式: {branch}_{short6}[optional '*']
: develop_0aedb4 / feature-ui_57448b*
- short6 = git rev-parse --short=6 HEAD小写 hex
- branch 清洗为 [A-Za-z0-9.-]整串不含 dirty '*') 24
- dirty working tree 时末尾追加 '*'
用法IAR Pre-build:
python \"$PROJ_DIR$\\..\\..\\tools\\gen_git_user_fw_ver.py\"
"""
from __future__ import annotations
import os
import re
import subprocess
ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
OUT = os.path.join(ROOT, "protocol", "git_user_fw_ver.h")
BUILD_ID_MAX = 24 # without trailing dirty '*'
def _git(*args: str) -> str | None:
try:
return subprocess.check_output(
["git", *args],
cwd=ROOT,
stderr=subprocess.DEVNULL,
text=True,
).strip()
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return None
def sanitize_branch(name: str) -> str:
if not name or name == "HEAD":
return "DETACHED"
# feature/foo → feature-foo去掉其它非法字符
s = name.replace("/", "-")
s = re.sub(r"[^A-Za-z0-9.-]+", "-", s)
s = re.sub(r"-{2,}", "-", s).strip("-.")
return s or "DETACHED"
def make_build_id(branch: str, short6: str) -> str:
"""Ensure '{branch}_{short6}' length ≤ BUILD_ID_MAX; keep commit suffix intact."""
short6 = short6.lower()[:6].ljust(6, "0")
suffix = "_" + short6
max_br = BUILD_ID_MAX - len(suffix)
if max_br < 1:
return short6[:BUILD_ID_MAX]
br = branch[:max_br]
return br + suffix
def main() -> int:
full = _git("rev-parse", "HEAD") or ("0" * 40)
if len(full) < 6 or not all(c in "0123456789abcdefABCDEF" for c in full):
full = "0" * 40
full = full.lower()
short6 = full[:6]
br_raw = _git("rev-parse", "--abbrev-ref", "HEAD") or "DETACHED"
branch = sanitize_branch(br_raw)
dirty = False
st = _git("status", "--porcelain")
if st:
dirty = True
build_id = make_build_id(branch, short6)
wire = build_id + ("*" if dirty else "")
# C string escape
wire_c = wire.replace("\\", "\\\\").replace('"', '\\"')
text = f"""/* Auto-generated by tools/gen_git_user_fw_ver.py — do not edit.
* git HEAD{' (dirty)' if dirty else ''}: {full}
* branch: {br_raw} {branch}
* 01 0C wire: {wire}
*/
#ifndef GIT_USER_FW_VER_H
#define GIT_USER_FW_VER_H
#define GIT_COMMIT_ID_FULL "{full}"
#define GIT_BRANCH_NAME "{branch}"
#define GIT_COMMIT_SHORT6 "{short6}"
#define GIT_DIRTY ({1 if dirty else 0})
#define GIT_BUILD_ID "{wire_c}"
#define GIT_BUILD_ID_LEN {len(wire)}u
#endif /* GIT_USER_FW_VER_H */
"""
os.makedirs(os.path.dirname(OUT), exist_ok=True)
old = ""
if os.path.isfile(OUT):
with open(OUT, "r", encoding="utf-8") as f:
old = f.read()
if old != text:
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
f.write(text)
print(f"gen_git_user_fw_ver: wrote {OUT} ({wire})")
else:
print(f"gen_git_user_fw_ver: up-to-date ({wire})")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -140,6 +140,21 @@ def expect(resp, prefix, length=None):
return True, hexs(resp) return True, hexs(resp)
def fmt_ver4(payload: bytes, *, kind: str = "semver") -> str:
"""解码版本字节为可读串。
kind:
semver 3 字节 major.minor.patch01 03/0C
date YY.M.D 日期01 04 音源 / 01 05 UI并展开为 20YY-MM-DD
"""
if len(payload) != 4:
return f"ver_raw={payload!r}"
a, b, c, _d = payload
if kind == "date":
return f"ver={a}.{b}.{c} date=20{a:02d}-{b:02d}-{c:02d}"
return f"ver={a}.{b}.{c}"
def run_tests(sim, allow_poweroff=False): def run_tests(sim, allow_poweroff=False):
R = Results() R = Results()
@ -156,12 +171,39 @@ def run_tests(sim, allow_poweroff=False):
d += f" name={name!r}" d += f" name={name!r}"
R.add("01 02 设备名", "PASS" if ok else "FAIL", d) R.add("01 02 设备名", "PASS" if ok else "FAIL", d)
for sub, name in ((0x03, "固件主版本"), (0x04, "音源版本"), for sub, name, kind in (
(0x05, "UI版本"), (0x0C, "用户固件版本")): (0x03, "固件主版本", "semver"),
(0x04, "音源版本", "date"),
(0x05, "UI版本", "date"),
):
r = sim.query(build(0x01, sub)) r = sim.query(build(0x01, sub))
ok, d = expect(r, (0x60, 0x01, sub), 9) ok, d = expect(r, (0x60, 0x01, sub), 9)
if ok:
d += f" {fmt_ver4(bytes(r[4:-1]), kind=kind)}"
R.add(f"01 {sub:02X} {name}", "PASS" if ok else "FAIL", d) R.add(f"01 {sub:02X} {name}", "PASS" if ok else "FAIL", d)
# 01 0C{branch}_{short6}[optional '*'],如 develop_0aedb4
r = sim.query(build(0x01, 0x0C))
ok, d = expect(r, (0x60, 0x01, 0x0C))
if ok:
raw = bytes(r[4:-1])
ok = (
3 <= len(raw) <= 25
and all(0x20 <= c < 0x7F for c in raw)
and b"_" in raw.rstrip(b"*")
)
build_s = raw.decode("ascii", "replace")
dirty = build_s.endswith("*")
core = build_s[:-1] if dirty else build_s
branch, _, short = core.rpartition("_")
extra = f" build={build_s!r}"
if len(short) == 6 and all(c in "0123456789abcdef" for c in short.lower()):
extra += f" branch={branch!r} commit={short.lower()}"
if dirty:
extra += " dirty=1"
d += extra
R.add("01 0C 用户固件版本", "PASS" if ok else "FAIL", d)
r = sim.query(build(0x01, 0x06)) r = sim.query(build(0x01, 0x06))
ok, d = expect(r, (0x60, 0x01, 0x06), 13) ok, d = expect(r, (0x60, 0x01, 0x06), 13)
R.add("01 06 其他信息", "PASS" if ok else "FAIL", d) R.add("01 06 其他信息", "PASS" if ok else "FAIL", d)
@ -341,8 +383,17 @@ def run_tests(sim, allow_poweroff=False):
# ---------------- 5. 复位/关机 0x05 ---------------- # ---------------- 5. 复位/关机 0x05 ----------------
if allow_poweroff: if allow_poweroff:
r = sim.query(build(0x05, 0x00), timeout=2.0) time.sleep(0.3)
# 禁止 query 自动重发:第二次会打在已断电的 BT 上
sim.drain()
sim.send(build(0x05, 0x00))
r = sim.read_frame(timeout=3.0)
ok, d = expect(r, (0x60, 0x05, 0x00), 6) ok, d = expect(r, (0x60, 0x05, 0x00), 6)
if not ok and r is None and hasattr(sim, "is_connected"):
# ACK 可能被软关机掐断 BT 抢走;链路断开也视为关机已执行
time.sleep(0.5)
if not sim.is_connected():
ok, d = True, "no ACK but BLE link dropped (soft-off likely)"
R.add("05 00 复位/关机", "PASS" if ok else "FAIL", d + " (设备将软关机)") R.add("05 00 复位/关机", "PASS" if ok else "FAIL", d + " (设备将软关机)")
else: else:
R.add("05 00 复位/关机", "SKIP", "需 --allow-poweroff") R.add("05 00 复位/关机", "SKIP", "需 --allow-poweroff")

View File

@ -270,17 +270,24 @@ class BleMidiSim:
while self.read_frame(timeout=quiet) is not None: while self.read_frame(timeout=quiet) is not None:
pass pass
def query(self, frame: bytes, timeout=3.0): def query(self, frame: bytes, timeout=3.0, retry=True):
self.drain(quiet=0.15) self.drain(quiet=0.15)
self.send(frame) self.send(frame)
r = self.read_frame(timeout=timeout) r = self.read_frame(timeout=timeout)
if r is None: if r is None and retry:
# BLE 偶发丢 Notify短间隔重发一次 # BLE 偶发丢 Notify短间隔重发一次(关机指令不可重试)
time.sleep(0.25) time.sleep(0.25)
self.send(frame) self.send(frame)
r = self.read_frame(timeout=timeout) r = self.read_frame(timeout=timeout)
return r return r
def is_connected(self) -> bool:
c = self._client
try:
return bool(c is not None and c.is_connected)
except Exception:
return False
async def _reconnect_clean(self, name, address, scan_timeout): async def _reconnect_clean(self, name, address, scan_timeout):
"""Drop session, unpair, rescan, reconnect — recovers Windows stale pairing.""" """Drop session, unpair, rescan, reconnect — recovers Windows stale pairing."""
await self._disconnect() await self._disconnect()
@ -345,7 +352,7 @@ def write_report(R: Results, info: dict, path: str, notes: list[str] | None = No
for note in notes or []: for note in notes or []:
lines.append(f"- {note}") lines.append(f"- {note}")
lines += [ lines += [
"- `01 03` 为 MCU 实际版本;`01 0F` 固件编码暂 `BRS08L`(原 `01 FF`BLE-MIDI 下 0xFF 非法)", "- `01 03`=`Version[]``01 04/05`=日期;`01 0C`=`branch_short6`(如 develop_0aedb4`01 0F`=`BRS08L`",
"- `01 07` UID 为 24 字符 hex ASCII96-bit保证 SysEx 7-bit 安全。", "- `01 07` UID 为 24 字符 hex ASCII96-bit保证 SysEx 7-bit 安全。",
"- `04 xx`/`06 xx` 无应答SENT 表示已发送。", "- `04 xx`/`06 xx` 无应答SENT 表示已发送。",
"- `FD 01` 升级指令不在本协议范围。", "- `FD 01` 升级指令不在本协议范围。",