Compare commits
60 Commits
main
...
feature_ba
| Author | SHA1 | Date |
|---|---|---|
|
|
fe034e3ffe | |
|
|
d4c35b8b90 | |
|
|
ba00940135 | |
|
|
d11d217b02 | |
|
|
11efccc19e | |
|
|
125eb00aa3 | |
|
|
65b0a84c4a | |
|
|
35d4cc28af | |
|
|
6fbeaa4881 | |
|
|
5f41af27e7 | |
|
|
2d9f234a7d | |
|
|
61caeb7407 | |
|
|
eed22d4b8c | |
|
|
c6be9fe88b | |
|
|
8ac13ce6e9 | |
|
|
cae4eb84a1 | |
|
|
4c6e2122ed | |
|
|
d06e44bcb3 | |
|
|
f8aeb4fb7c | |
|
|
7fa2f5f1c4 | |
|
|
6da7d4d627 | |
|
|
eb1e5cbe84 | |
|
|
28970fddf0 | |
|
|
a495717883 | |
|
|
daee47a457 | |
|
|
f841d9e7b1 | |
|
|
713a768733 | |
|
|
24261942d8 | |
|
|
1cf6562236 | |
|
|
91b270d856 | |
|
|
439ccf439d | |
|
|
2d62e1e41a | |
|
|
4aad989263 | |
|
|
295a2a6fa3 | |
|
|
b65b762d92 | |
|
|
30c4ea9c0d | |
|
|
05ccf28a4b | |
|
|
59c11f3f93 | |
|
|
24ffe87527 | |
|
|
5bb403cd11 | |
|
|
a818065c2e | |
|
|
c1b32c0351 | |
|
|
bb6d9ae7f9 | |
|
|
99f7ed3e5a | |
|
|
cd21e4e45e | |
|
|
92e5e32de3 | |
|
|
015417dc3d | |
|
|
6f2f7b6b75 | |
|
|
ba110565f1 | |
|
|
ad0d990c55 | |
|
|
f1f9520637 | |
|
|
57448b3da8 | |
|
|
0aedb45e6e | |
|
|
7b08f5e098 | |
|
|
f762348883 | |
|
|
8febbd7742 | |
|
|
bd22246942 | |
|
|
725ac495ba | |
|
|
01bc2a592c | |
|
|
a6259b5fca |
|
|
@ -40,3 +40,7 @@ Desktop.ini
|
||||||
*.key
|
*.key
|
||||||
*.pem
|
*.pem
|
||||||
credentials.json
|
credentials.json
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,9 @@ static void app_adc_charge_process(void)
|
||||||
{
|
{
|
||||||
/* 通知 UI 熄灭充电页并关主电 */
|
/* 通知 UI 熄灭充电页并关主电 */
|
||||||
MainTask_Sendmsg(MSG_ID_POWOFF_CHARG, percentage, 0, 0);
|
MainTask_Sendmsg(MSG_ID_POWOFF_CHARG, percentage, 0, 0);
|
||||||
BSP_MainPowerEnable(0);
|
/* 发消息可能让出 CPU:关电前再确认仍未开机,避免切断开机主电 */
|
||||||
|
if (!powon)
|
||||||
|
BSP_MainPowerEnable(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -303,6 +305,14 @@ void app_adc_sync_volume(uint16_t val)
|
||||||
app_adc_volume_map_level(val);
|
app_adc_volume_map_level(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 取当前主音量(复用扫描任务维护的最近一次有效值)。
|
||||||
|
* 模拟开关同一时刻只选中一路,离开扫描上下文直接重读 ADC 会串到
|
||||||
|
* 其它通道,故统一走 vol_last;未初始化(0xFFFF)时按静音兜底 */
|
||||||
|
uint8_t app_adc_get_volume(void)
|
||||||
|
{
|
||||||
|
return (vol_last > 127) ? 0 : (uint8_t)vol_last;
|
||||||
|
}
|
||||||
|
|
||||||
static void app_adc_volume_process(void)
|
static void app_adc_volume_process(void)
|
||||||
{
|
{
|
||||||
uint16_t val = ADC_ReadChannel(ADC_CHANNEL_1)>>5;
|
uint16_t val = ADC_ReadChannel(ADC_CHANNEL_1)>>5;
|
||||||
|
|
|
||||||
|
|
@ -27,4 +27,6 @@ void app_adc_in1_scan_part(uint8_t channel);
|
||||||
bool app_adc_usb_is_plugged(void);
|
bool app_adc_usb_is_plugged(void);
|
||||||
/* 开机/主动采样后同步 vol_last 与 level,避免扫描任务误触发或跳变 */
|
/* 开机/主动采样后同步 vol_last 与 level,避免扫描任务误触发或跳变 */
|
||||||
void app_adc_sync_volume(uint16_t val);
|
void app_adc_sync_volume(uint16_t val);
|
||||||
|
/* 取当前主音量(0~127),供 MasterVolume_Reapply 等重发场景使用 */
|
||||||
|
uint8_t app_adc_get_volume(void);
|
||||||
#endif
|
#endif
|
||||||
254
APP/app_log.c
254
APP/app_log.c
|
|
@ -11,7 +11,7 @@ static uint32_t s_overflow;
|
||||||
static uint32_t s_boot_count;
|
static uint32_t s_boot_count;
|
||||||
static char s_ui_page[16] = "boot";
|
static char s_ui_page[16] = "boot";
|
||||||
static char s_debug_level = 'D';
|
static char s_debug_level = 'D';
|
||||||
static char s_cmd_buf[48];
|
static char s_cmd_buf[64];
|
||||||
static uint8_t s_cmd_len;
|
static uint8_t s_cmd_len;
|
||||||
|
|
||||||
/* RTT 二进制烧录:写到 W25Q128(ui0902 或本地曲目) */
|
/* RTT 二进制烧录:写到 W25Q128(ui0902 或本地曲目) */
|
||||||
|
|
@ -136,17 +136,36 @@ void app_log_set_ui_page(const char *name)
|
||||||
|
|
||||||
void app_log_write(char level, const char *cat, const char *fmt, ...)
|
void app_log_write(char level, const char *cat, const char *fmt, ...)
|
||||||
{
|
{
|
||||||
char line[APP_LOG_SLOT_SIZE];
|
/* 静态行缓冲:TaskBTRecv 等小栈线程禁止再开 128B 局部数组 */
|
||||||
|
static char line[APP_LOG_SLOT_SIZE];
|
||||||
|
static rt_mutex_t s_log_mtx = RT_NULL;
|
||||||
va_list ap;
|
va_list ap;
|
||||||
int n;
|
int n;
|
||||||
|
|
||||||
|
/* TMR6 1ms ISR 会经 AutoBand NoteOn 回调进来:禁止在中断里 take mutex / 打 RTT */
|
||||||
|
if (rt_interrupt_get_nest() != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (level == 'D' && s_debug_level != 'D') {
|
if (level == 'D' && s_debug_level != 'D') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (s_log_mtx == RT_NULL) {
|
||||||
|
s_log_mtx = rt_mutex_create("applog", RT_IPC_FLAG_PRIO);
|
||||||
|
}
|
||||||
|
if (s_log_mtx != RT_NULL) {
|
||||||
|
if (rt_mutex_take(s_log_mtx, rt_tick_from_millisecond(20)) != RT_EOK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
n = snprintf(line, sizeof(line), "[%05u][%c][%-4s] ",
|
n = snprintf(line, sizeof(line), "[%05u][%c][%-4s] ",
|
||||||
(unsigned)app_log_ms(), level, cat);
|
(unsigned)app_log_ms(), level, cat);
|
||||||
if (n < 0) {
|
if (n < 0) {
|
||||||
|
if (s_log_mtx != RT_NULL) {
|
||||||
|
rt_mutex_release(s_log_mtx);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((size_t)n >= sizeof(line)) {
|
if ((size_t)n >= sizeof(line)) {
|
||||||
|
|
@ -160,6 +179,10 @@ void app_log_write(char level, const char *cat, const char *fmt, ...)
|
||||||
app_log_slot_store(line);
|
app_log_slot_store(line);
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "\n");
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "\n");
|
||||||
|
|
||||||
|
if (s_log_mtx != RT_NULL) {
|
||||||
|
rt_mutex_release(s_log_mtx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void app_log_dump_ram(void)
|
static void app_log_dump_ram(void)
|
||||||
|
|
@ -274,12 +297,24 @@ uint8_t app_log_try_command(const char *cmd)
|
||||||
app_log_flash_start(size);
|
app_log_flash_start(size);
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
|
/* flash all <size> → W25Q128 @ 0x0(tone+UI 合成 ALL.res) */
|
||||||
|
if (strncmp(cmd, "flash all ", 10) == 0) {
|
||||||
|
uint32_t size = (uint32_t)strtoul(cmd + 10, NULL, 10);
|
||||||
|
app_log_flash_start_at(0U, size, "FLASH_ALL_GO", "FLASH_ALL_OK\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
if (strncmp(cmd, "flash haitian ", 14) == 0) {
|
if (strncmp(cmd, "flash haitian ", 14) == 0) {
|
||||||
uint32_t size = (uint32_t)strtoul(cmd + 14, NULL, 10);
|
uint32_t size = (uint32_t)strtoul(cmd + 14, NULL, 10);
|
||||||
app_log_flash_start_at(FLASH_ADDR_SONG_HAITIAN, size,
|
app_log_flash_start_at(FLASH_ADDR_SONG_HAITIAN, size,
|
||||||
"FLASH_HAITIAN_GO", "FLASH_HAITIAN_OK\n");
|
"FLASH_HAITIAN_GO", "FLASH_HAITIAN_OK\n");
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
|
if (strncmp(cmd, "flash bin3 ", 11) == 0) {
|
||||||
|
uint32_t size = (uint32_t)strtoul(cmd + 11, NULL, 10);
|
||||||
|
app_log_flash_start_at(EXTFLASH_BIN3_UNIVERSAL_ADDR, size,
|
||||||
|
"FLASH_BIN3_GO", "FLASH_BIN3_OK\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
if (strncmp(cmd, "tone status", 11) == 0) {
|
if (strncmp(cmd, "tone status", 11) == 0) {
|
||||||
uint8_t hdr[16];
|
uint8_t hdr[16];
|
||||||
uint8_t name[40];
|
uint8_t name[40];
|
||||||
|
|
@ -320,6 +355,16 @@ uint8_t app_log_try_command(const char *cmd)
|
||||||
(unsigned long)(cur_hdr[12] | (cur_hdr[13] << 8) |
|
(unsigned long)(cur_hdr[12] | (cur_hdr[13] << 8) |
|
||||||
(cur_hdr[14] << 16) | (cur_hdr[15] << 24)));
|
(cur_hdr[14] << 16) | (cur_hdr[15] << 24)));
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
{
|
||||||
|
uint8_t b3[16];
|
||||||
|
W25Q128_Read(b3, EXTFLASH_BIN3_UNIVERSAL_ADDR, 16);
|
||||||
|
snprintf(line, sizeof(line),
|
||||||
|
"TONE @BIN3 magic=%02X%02X%02X%02X cnt=%lu\n",
|
||||||
|
b3[0], b3[1], b3[2], b3[3],
|
||||||
|
(unsigned long)(b3[12] | (b3[13] << 8) |
|
||||||
|
(b3[14] << 16) | (b3[15] << 24)));
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
}
|
||||||
snprintf(line, sizeof(line),
|
snprintf(line, sizeof(line),
|
||||||
"TONE @BIN2 magic=%02X%02X%02X%02X max=%lu cnt=%lu name=%.16s\n",
|
"TONE @BIN2 magic=%02X%02X%02X%02X max=%lu cnt=%lu name=%.16s\n",
|
||||||
hdr[0], hdr[1], hdr[2], hdr[3],
|
hdr[0], hdr[1], hdr[2], hdr[3],
|
||||||
|
|
@ -353,7 +398,7 @@ uint8_t app_log_try_command(const char *cmd)
|
||||||
UI_ApplyToneAddress();
|
UI_ApplyToneAddress();
|
||||||
AutoBandTop1_Stop();
|
AutoBandTop1_Stop();
|
||||||
StartFlag = 0;
|
StartFlag = 0;
|
||||||
ret = AutoBandTop1_LoadPresetItemFromFlash(0);
|
ret = App_Auto_LoadPresetFromFlash(0);
|
||||||
snprintf(line, sizeof(line),
|
snprintf(line, sizeof(line),
|
||||||
"TONE_LOCAL ret=%d addr=0x%08lX map=BIN2@0x%08lX name=%s count=%d %s\n",
|
"TONE_LOCAL ret=%d addr=0x%08lX map=BIN2@0x%08lX name=%s count=%d %s\n",
|
||||||
ret, (unsigned long)ADDRESS,
|
ret, (unsigned long)ADDRESS,
|
||||||
|
|
@ -364,16 +409,203 @@ uint8_t app_log_try_command(const char *cmd)
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
|
/* tone bin1 [idx] → 1.bin 节奏(专业+节奏类型) */
|
||||||
|
if (strncmp(cmd, "tone bin1", 9) == 0) {
|
||||||
|
char line[160];
|
||||||
|
int ret;
|
||||||
|
unsigned idx = 0;
|
||||||
|
int count;
|
||||||
|
if (cmd[9] == ' ' && cmd[10] != '\0')
|
||||||
|
idx = (unsigned)strtoul(cmd + 10, NULL, 10);
|
||||||
|
mGuiData[GUI_TAB_INDEX].Current = 1; /* 专业 */
|
||||||
|
mGuiData[GUI_AUTOBAND_SW].Current = 0; /* 节奏类型→1.bin */
|
||||||
|
ParamGuiData[SONG_MODE_PARAM].Current = (uint8_t)idx;
|
||||||
|
UI_ApplyToneAddress();
|
||||||
|
AutoBandTop1_Stop();
|
||||||
|
StartFlag = 0;
|
||||||
|
count = AutoBandTop1_GetPresetItemCount();
|
||||||
|
if (count > 0 && (int)idx >= count)
|
||||||
|
idx = (unsigned)(count - 1);
|
||||||
|
ParamGuiData[SONG_MODE_PARAM].Current = (uint8_t)idx;
|
||||||
|
ret = App_Auto_LoadPresetFromFlash((int)idx);
|
||||||
|
snprintf(line, sizeof(line),
|
||||||
|
"TONE_BIN1 ret=%d idx=%u addr=0x%08lX map=BIN1@0x%08lX name=%s count=%d %s\n",
|
||||||
|
ret, idx, (unsigned long)ADDRESS,
|
||||||
|
(unsigned long)EXTFLASH_BIN1_RHYTHM_ADDR,
|
||||||
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "(null)",
|
||||||
|
AutoBandTop1_GetPresetItemCount(),
|
||||||
|
(ADDRESS == EXTFLASH_BIN1_RHYTHM_ADDR) ? "OK" : "MISMATCH");
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
/* tone bin2 [idx] → 2.bin 本地曲目(专业+本地曲目,带索引扫描) */
|
||||||
|
if (strncmp(cmd, "tone bin2", 9) == 0) {
|
||||||
|
char line[160];
|
||||||
|
int ret;
|
||||||
|
unsigned idx = 0;
|
||||||
|
int count;
|
||||||
|
if (cmd[9] == ' ' && cmd[10] != '\0')
|
||||||
|
idx = (unsigned)strtoul(cmd + 10, NULL, 10);
|
||||||
|
mGuiData[GUI_TAB_INDEX].Current = 1; /* 专业 */
|
||||||
|
mGuiData[GUI_AUTOBAND_SW].Current = 1; /* 本地曲目→2.bin */
|
||||||
|
ParamGuiData[EXPRESS_MODE_PARAM].Current = (uint8_t)idx;
|
||||||
|
UI_ApplyToneAddress();
|
||||||
|
AutoBandTop1_Stop();
|
||||||
|
StartFlag = 0;
|
||||||
|
count = AutoBandTop1_GetPresetItemCount();
|
||||||
|
if (count > 0 && (int)idx >= count)
|
||||||
|
idx = (unsigned)(count - 1);
|
||||||
|
ParamGuiData[EXPRESS_MODE_PARAM].Current = (uint8_t)idx;
|
||||||
|
ret = App_Auto_LoadPresetFromFlash((int)idx);
|
||||||
|
snprintf(line, sizeof(line),
|
||||||
|
"TONE_BIN2 ret=%d idx=%u addr=0x%08lX map=BIN2@0x%08lX name=%s count=%d %s\n",
|
||||||
|
ret, idx, (unsigned long)ADDRESS,
|
||||||
|
(unsigned long)EXTFLASH_BIN2_SONG_HAITIAN_ADDR,
|
||||||
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "(null)",
|
||||||
|
AutoBandTop1_GetPresetItemCount(),
|
||||||
|
(ADDRESS == EXTFLASH_BIN2_SONG_HAITIAN_ADDR) ? "OK" : "MISMATCH");
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
/* tone bin3 [idx] → 3.bin 万能和弦走向 */
|
||||||
|
if (strncmp(cmd, "tone bin3", 9) == 0) {
|
||||||
|
char line[160];
|
||||||
|
int ret;
|
||||||
|
unsigned idx = 0;
|
||||||
|
int count;
|
||||||
|
if (cmd[9] == ' ' && cmd[10] != '\0')
|
||||||
|
idx = (unsigned)strtoul(cmd + 10, NULL, 10);
|
||||||
|
mGuiData[GUI_TAB_INDEX].Current = 0; /* 万能 */
|
||||||
|
mGuiData[GUI_AUTOBAND_SW].Current = 0;
|
||||||
|
ParamGuiData[ALL_MODE_PARAM].Current = (uint8_t)idx;
|
||||||
|
UI_ApplyToneAddress();
|
||||||
|
AutoBandTop1_Stop();
|
||||||
|
StartFlag = 0;
|
||||||
|
count = AutoBandTop1_GetPresetItemCount();
|
||||||
|
if (count > 0 && (int)idx >= count)
|
||||||
|
idx = (unsigned)(count - 1);
|
||||||
|
ParamGuiData[ALL_MODE_PARAM].Current = (uint8_t)idx;
|
||||||
|
ret = App_Auto_LoadPresetFromFlash((int)idx);
|
||||||
|
snprintf(line, sizeof(line),
|
||||||
|
"TONE_BIN3 ret=%d idx=%u addr=0x%08lX map=BIN3@0x%08lX name=%s count=%d %s\n",
|
||||||
|
ret, idx, (unsigned long)ADDRESS,
|
||||||
|
(unsigned long)EXTFLASH_BIN3_UNIVERSAL_ADDR,
|
||||||
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "(null)",
|
||||||
|
AutoBandTop1_GetPresetItemCount(),
|
||||||
|
(ADDRESS == EXTFLASH_BIN3_UNIVERSAL_ADDR) ? "OK" : "MISMATCH");
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
/* tone start:在当前已加载音色上拨片起奏(不改 TAB/库) */
|
||||||
|
if (strncmp(cmd, "tone start", 10) == 0) {
|
||||||
|
StartFlag = 0;
|
||||||
|
if (!(cmd[10] == ' ' && (cmd[11] == 'k' || cmd[11] == 'K')))
|
||||||
|
KEY_ID_1629 = 0;
|
||||||
|
Pick_Handle();
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_START_DONE\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
if (strncmp(cmd, "tone pick", 9) == 0) {
|
if (strncmp(cmd, "tone pick", 9) == 0) {
|
||||||
/* 模拟专业+本地曲目拨片(无和弦板) */
|
/* tone pick uni → 万能;tone pick free → 普通+节奏;默认专业+本地曲目 */
|
||||||
|
if (cmd[9] == ' ' && (cmd[10] == 'u' || cmd[10] == 'U')) {
|
||||||
|
mGuiData[GUI_TAB_INDEX].Current = 0;
|
||||||
|
mGuiData[GUI_AUTOBAND_SW].Current = 0;
|
||||||
|
UI_ApplyToneAddress();
|
||||||
|
AutoBandTop1_Stop();
|
||||||
|
StartFlag = 0;
|
||||||
|
(void)App_Auto_LoadPresetFromFlash(ParamGuiData[ALL_MODE_PARAM].Current);
|
||||||
|
KEY_ID_1629 = 0;
|
||||||
|
PressFlag = 0;
|
||||||
|
Pick_Handle();
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_PICK_UNI_DONE\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
if (cmd[9] == ' ' && (cmd[10] == 'f' || cmd[10] == 'F' || cmd[10] == 'n' || cmd[10] == 'N')) {
|
||||||
|
/* 普通模式 + 节奏类型(1.bin);模拟已按指板 */
|
||||||
|
unsigned idx = ParamGuiData[SONG_MODE_PARAM].Current;
|
||||||
|
mGuiData[GUI_TAB_INDEX].Current = 2;
|
||||||
|
mGuiData[GUI_AUTOBAND_SW].Current = 0;
|
||||||
|
UI_ApplyToneAddress();
|
||||||
|
AutoBandTop1_Stop();
|
||||||
|
StartFlag = 0;
|
||||||
|
(void)App_Auto_LoadPresetFromFlash((int)idx);
|
||||||
|
if (!(cmd[11] == ' ' && (cmd[12] == 'k' || cmd[12] == 'K')))
|
||||||
|
KEY_ID_1629 = 1;
|
||||||
|
PressFlag = 1;
|
||||||
|
Pick_Handle();
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_PICK_FREE_DONE\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
/* 模拟专业+本地曲目拨片;chord pick keep → 保留已注入的 KEY_ID */
|
||||||
mGuiData[GUI_TAB_INDEX].Current = 1;
|
mGuiData[GUI_TAB_INDEX].Current = 1;
|
||||||
mGuiData[GUI_AUTOBAND_SW].Current = 1;
|
mGuiData[GUI_AUTOBAND_SW].Current = 1;
|
||||||
KEY_ID_1629 = 0; /* 走默认和弦兜底 */
|
if (!(cmd[9] == ' ' && (cmd[10] == 'k' || cmd[10] == 'K'))) {
|
||||||
|
KEY_ID_1629 = 0; /* 默认走一级和弦兜底 */
|
||||||
|
}
|
||||||
StartFlag = 0;
|
StartFlag = 0;
|
||||||
Pick_Handle();
|
Pick_Handle();
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_PICK_DONE\n");
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_PICK_DONE\n");
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
|
/* 测试注入:chord key N(0释放 / 1~21和弦 / 22拍速 / 23停止) */
|
||||||
|
if (strncmp(cmd, "chord key ", 10) == 0 && cmd[10] != '\0') {
|
||||||
|
unsigned key = (unsigned)strtoul(cmd + 10, NULL, 10);
|
||||||
|
char line[48];
|
||||||
|
int hold = (strstr(cmd + 10, "hold") != NULL);
|
||||||
|
if (key > 23U) {
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "CHORD_KEY_BAD\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
if (hold)
|
||||||
|
app_tm1629_inject_hold((uint8_t)key);
|
||||||
|
else
|
||||||
|
app_tm1629_inject_key((uint8_t)key);
|
||||||
|
snprintf(line, sizeof(line), "CHORD_KEY_OK key=%u%s\n", key, hold ? " hold" : "");
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
/* 测试注入:tm1617 key N(0~3=MAIN_D/C/B/A 段落或导航键,4=释放)
|
||||||
|
* ACK 先于 Handle:0~3 会投递触摸消息,可能挤占 RTT 下行导致 ACK 丢失 */
|
||||||
|
if (strncmp(cmd, "tm1617 key ", 11) == 0 && cmd[11] != '\0') {
|
||||||
|
unsigned key = (unsigned)strtoul(cmd + 11, NULL, 10);
|
||||||
|
char line[48];
|
||||||
|
if (key > 4U) {
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TM1617_KEY_BAD\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
snprintf(line, sizeof(line), "TM1617_KEY_OK key=%u\n", key);
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
app_tm1617_inject_key((uint8_t)key);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
/* 测试注入:adc key N on|off(0~3;1=独立尾奏键,与万能第4键同路径) */
|
||||||
|
if (strncmp(cmd, "adc key ", 8) == 0 && cmd[8] != '\0') {
|
||||||
|
unsigned idx = (unsigned)strtoul(cmd + 8, NULL, 10);
|
||||||
|
char line[48];
|
||||||
|
int on = (strstr(cmd + 8, "on") != NULL);
|
||||||
|
if (idx > 3U || (!on && strstr(cmd + 8, "off") == NULL)) {
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "ADC_KEY_BAD\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
snprintf(line, sizeof(line), "ADC_KEY_OK idx=%u on=%d\n", idx, on);
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
ADC_IN1_KEY_Handle((uint8_t)idx, on ? true : false);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
/* 测试注入:chord xpose N(0~11,C=0) */
|
||||||
|
if (strncmp(cmd, "chord xpose ", 12) == 0 && cmd[12] != '\0') {
|
||||||
|
unsigned xp = (unsigned)strtoul(cmd + 12, NULL, 10);
|
||||||
|
char line[48];
|
||||||
|
if (xp > 11U) {
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "CHORD_XPOSE_BAD\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
mGuiData[GUI_TRANSPOSE].Current = (uint8_t)xp;
|
||||||
|
snprintf(line, sizeof(line), "CHORD_XPOSE_OK xp=%u\n", xp);
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
LOG_I("REG", "xpose set %u", xp);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
if (strncmp(cmd, "flash erase chip", 16) == 0) {
|
if (strncmp(cmd, "flash erase chip", 16) == 0) {
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "FLASH_ERASE_BEGIN\n");
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "FLASH_ERASE_BEGIN\n");
|
||||||
LOG_I("FLASH", "W25Q128 chip erase start");
|
LOG_I("FLASH", "W25Q128 chip erase start");
|
||||||
|
|
@ -383,24 +615,28 @@ uint8_t app_log_try_command(const char *cmd)
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
if (strncmp(cmd, "ui boot", 7) == 0) {
|
if (strncmp(cmd, "ui boot", 7) == 0) {
|
||||||
|
/* 先 ACK:全屏 Logo 绘制很慢,避免 HIL/主机超时误判 */
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "UI_BOOT_OK\n");
|
||||||
LCD_BLK_Set();
|
LCD_BLK_Set();
|
||||||
LCD_WR_PIC_FROM_FLASH(0, 0, UI0902_BOOT_LOGO_W, UI0902_BOOT_LOGO_H,
|
LCD_WR_PIC_FROM_FLASH(0, 0, UI0902_BOOT_LOGO_W, UI0902_BOOT_LOGO_H,
|
||||||
UI0902_BOOT_LOGO_ADDR);
|
UI0902_BOOT_LOGO_ADDR);
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "UI_BOOT_OK\n");
|
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
if (strncmp(cmd, "ui charge", 9) == 0) {
|
if (strncmp(cmd, "ui charge", 9) == 0) {
|
||||||
uint8_t peek[4];
|
uint8_t peek[4];
|
||||||
|
W25Q128_Read(peek, UI0902_CHARGE_SCREEN_ADDR, 4);
|
||||||
|
/* 先 ACK(peek 很快);再铺色/画图,避免卡在 LCD SPI 时主机无响应 */
|
||||||
|
if (peek[0] != 0xFF || peek[1] != 0xFF)
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "UI_CHARGE_OK\n");
|
||||||
|
else
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "UI_CHARGE_EMPTY\n");
|
||||||
LCD_BLK_Set();
|
LCD_BLK_Set();
|
||||||
LCD_FillByColor(0, 0, 240, 320, UI0902_BG_COLOR);
|
LCD_FillByColor(0, 0, 240, 320, UI0902_BG_COLOR);
|
||||||
W25Q128_Read(peek, UI0902_CHARGE_SCREEN_ADDR, 4);
|
|
||||||
if (peek[0] != 0xFF || peek[1] != 0xFF) {
|
if (peek[0] != 0xFF || peek[1] != 0xFF) {
|
||||||
LCD_WR_PIC_FROM_FLASH(0, 0, UI0902_CHARGE_SCREEN_W, UI0902_CHARGE_SCREEN_H,
|
LCD_WR_PIC_FROM_FLASH(0, 0, UI0902_CHARGE_SCREEN_W, UI0902_CHARGE_SCREEN_H,
|
||||||
UI0902_CHARGE_SCREEN_ADDR);
|
UI0902_CHARGE_SCREEN_ADDR);
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "UI_CHARGE_OK\n");
|
|
||||||
} else {
|
} else {
|
||||||
Draw_Charging_Icon(120, 150, 2, 0x3D7F);
|
Draw_Charging_Icon(120, 150, 2, 0x3D7F);
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "UI_CHARGE_EMPTY\n");
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const char *s = "50";
|
const char *s = "50";
|
||||||
|
|
|
||||||
|
|
@ -84,3 +84,13 @@ uint8_t app_tm1617_scan_key(void)
|
||||||
}
|
}
|
||||||
return KEY_NULL;
|
return KEY_NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void app_tm1617_inject_key(uint8_t key)
|
||||||
|
{
|
||||||
|
if (key >= KEY_NULL)
|
||||||
|
key = KEY_NULL;
|
||||||
|
key_last = key;
|
||||||
|
LOG_I("KEY", "tm1617 inject key=%u", (unsigned)key);
|
||||||
|
/* ???????? RTT poll ?????? UI/?????????? */
|
||||||
|
MainTask_Sendmsg(MSG_ID_KEY_1617, key, 0, 0);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,8 @@ void app_tm1617_auto_loop(void);
|
||||||
|
|
||||||
uint8_t app_tm1617_scan_key(void);
|
uint8_t app_tm1617_scan_key(void);
|
||||||
|
|
||||||
|
/* RTT/test: bypass physical scan and inject a key event directly.
|
||||||
|
* key: 0=KEY_MAIN_D 1=KEY_MAIN_C 2=KEY_MAIN_B 3=KEY_MAIN_A 4+=release(KEY_NULL) */
|
||||||
|
void app_tm1617_inject_key(uint8_t key);
|
||||||
|
|
||||||
#endif /* APP_TM1617_H */
|
#endif /* APP_TM1617_H */
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
#include "includes.h"
|
#include "includes.h"
|
||||||
|
|
||||||
static uint8_t key_last = KEY_NONE;
|
static uint8_t key_last = KEY_NONE;
|
||||||
|
static uint8_t key_hold_sticky = 0; /* 1 = ????????????????(KEY_NONE)????? */
|
||||||
|
|
||||||
|
|
||||||
/* LED 期望颜色缓存:索引 = LedNum_TypeDef(0~7),COLOR_OTHER 表示灭 */
|
/* LED ???????????????? = LedNum_TypeDef??0~7????COLOR_OTHER ????? */
|
||||||
//static LedColor_TypeDef led_cache[8] = {
|
//static LedColor_TypeDef led_cache[8] = {
|
||||||
// COLOR_OTHER, COLOR_OTHER, COLOR_OTHER, COLOR_OTHER,
|
// COLOR_OTHER, COLOR_OTHER, COLOR_OTHER, COLOR_OTHER,
|
||||||
// COLOR_OTHER, COLOR_OTHER, COLOR_OTHER, COLOR_OTHER
|
// COLOR_OTHER, COLOR_OTHER, COLOR_OTHER, COLOR_OTHER
|
||||||
|
|
@ -28,6 +29,9 @@ uint8_t app_tm1629_Scan_Key(void)
|
||||||
key = TM1629D_GetKey();
|
key = TM1629D_GetKey();
|
||||||
rt_mutex_release(TM1629_Mutex);
|
rt_mutex_release(TM1629_Mutex);
|
||||||
|
|
||||||
|
if(key_hold_sticky && key == KEY_NONE)
|
||||||
|
return KEY_NONE; /* ??????§ľ??????????????????????? */
|
||||||
|
|
||||||
if(key != key_last)
|
if(key != key_last)
|
||||||
{
|
{
|
||||||
key_last = key;
|
key_last = key;
|
||||||
|
|
@ -38,6 +42,27 @@ uint8_t app_tm1629_Scan_Key(void)
|
||||||
return KEY_NONE;
|
return KEY_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void app_tm1629_inject_key(uint8_t key)
|
||||||
|
{
|
||||||
|
key_hold_sticky = 0;
|
||||||
|
key_last = key;
|
||||||
|
LOG_I("KEY", "inject key=%u", (unsigned)key);
|
||||||
|
TM1629_Handle(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
void app_tm1629_inject_hold(uint8_t key)
|
||||||
|
{
|
||||||
|
if(key == KEY_NONE)
|
||||||
|
{
|
||||||
|
app_tm1629_inject_key(KEY_NONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
key_hold_sticky = 1;
|
||||||
|
key_last = key;
|
||||||
|
LOG_I("KEY", "inject hold key=%u", (unsigned)key);
|
||||||
|
TM1629_Handle(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void app_tm1629_led_set(LedNum_TypeDef led, LedColor_TypeDef color, uint8_t on)
|
static void app_tm1629_led_set(LedNum_TypeDef led, LedColor_TypeDef color, uint8_t on)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
void app_tm1629_init(void);
|
void app_tm1629_init(void);
|
||||||
uint8_t app_tm1629_Scan_Key(void);
|
uint8_t app_tm1629_Scan_Key(void);
|
||||||
|
void app_tm1629_inject_key(uint8_t key); /* RTT/测试:绕过扫描直接注入 */
|
||||||
|
void app_tm1629_inject_hold(uint8_t key); /* RTT/测试:注入并保持(物理空扫不释放,chord key 0 解除) */
|
||||||
void app_tm1629_set_chord_led(uint8_t key_idx, LedColor_TypeDef color);
|
void app_tm1629_set_chord_led(uint8_t key_idx, LedColor_TypeDef color);
|
||||||
void app_tm1629_set_led(LedNum_TypeDef led, LedColor_TypeDef color);
|
void app_tm1629_set_led(LedNum_TypeDef led, LedColor_TypeDef color);
|
||||||
void app_tm1629_all_off(void);
|
void app_tm1629_all_off(void);
|
||||||
|
|
|
||||||
504
Global/Global.c
504
Global/Global.c
|
|
@ -89,11 +89,11 @@ const Touch_AreaTypeDef Touch_Areas[] =
|
||||||
{ 126, 172, UI0902_SECTION_Y, UI0902_SECTION_Y + UI0902_SECTION_BTN_H - 1, 0, 2, GUI_PLAY_SECTION, NULL },
|
{ 126, 172, UI0902_SECTION_Y, UI0902_SECTION_Y + UI0902_SECTION_BTN_H - 1, 0, 2, GUI_PLAY_SECTION, NULL },
|
||||||
{ 183, 229, UI0902_SECTION_Y, UI0902_SECTION_Y + UI0902_SECTION_BTN_H - 1, 0, 3, GUI_PLAY_SECTION, NULL },
|
{ 183, 229, UI0902_SECTION_Y, UI0902_SECTION_Y + UI0902_SECTION_BTN_H - 1, 0, 3, GUI_PLAY_SECTION, NULL },
|
||||||
|
|
||||||
/* 底栏四键:万能 | 普通 | 专业 | 设置(触区中线对齐 cx 40/100/160/210) */
|
/* 底栏四键:万能 | 普通 | 专业 | 设置(60×55,y265–319) */
|
||||||
{ 0, 69, 280, 319, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(0), UI0902_NAV_X1(0), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
|
||||||
{ 70, 129, 280, 319, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(1), UI0902_NAV_X1(1), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
|
||||||
{ 130, 184, 280, 319, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(2), UI0902_NAV_X1(2), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
|
||||||
{ 185, 239, 280, 319, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(3), UI0902_NAV_X1(3), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
|
||||||
};
|
};
|
||||||
|
|
||||||
/* 推子 0~10 共 11 档;档10 取 AW816 上限附近 */
|
/* 推子 0~10 共 11 档;档10 取 AW816 上限附近 */
|
||||||
|
|
@ -101,11 +101,14 @@ int16_t MIC_VOL_MAP[11]= {-9000,-7000,-5000,-3000,-1000,1000,3000,5000,7000,9000
|
||||||
|
|
||||||
uint8_t TOUCH_AREA_NUM = (sizeof(Touch_Areas)/sizeof(Touch_AreaTypeDef));
|
uint8_t TOUCH_AREA_NUM = (sizeof(Touch_Areas)/sizeof(Touch_AreaTypeDef));
|
||||||
|
|
||||||
const uint8_t Version[3] = {0,2,6}; /* major.minor.patch — 0.2.6: local song 2.bin from Doc/音色文件/0908/2ss_2.bin */
|
const uint8_t Version[3] = {0,2,14}; /* major.minor.patch — 0.2.14: dual MIDI bank + toneRes 0916_bank */
|
||||||
|
|
||||||
uint8_t Led = 8;
|
uint8_t Led = 8;
|
||||||
bool PressFlag = 0;
|
bool PressFlag = 0;
|
||||||
|
|
||||||
|
void Return_Light_buff(uint8_t key_id_1629);
|
||||||
|
uint8_t Band_IsPlaying(void);
|
||||||
|
|
||||||
uint8_t AutoCloseTime = 15;
|
uint8_t AutoCloseTime = 15;
|
||||||
uint16_t mAutoPowerOffCount = 15 * 60;
|
uint16_t mAutoPowerOffCount = 15 * 60;
|
||||||
bool AutoCloseFlag = true;
|
bool AutoCloseFlag = true;
|
||||||
|
|
@ -159,8 +162,9 @@ void BLTask_Sendmsg(uint16_t ID, uint16_t ID2, uint16_t HiByte, uint16_t LoByte)
|
||||||
|
|
||||||
#define KEY_PRESS_LEVEL RESET
|
#define KEY_PRESS_LEVEL RESET
|
||||||
|
|
||||||
#define PWR_ON_HOLD_MS 3000 /* 关机态长按 3s → 开机 */
|
#define PWR_ON_HOLD_MS 3000 /* 关机态长按 3s → 开机 */
|
||||||
#define PWR_OFF_HOLD_MS 2000 /* 开机态长按 2s → 关机 */
|
#define PWR_OFF_HOLD_MS 2000 /* 开机态长按 2s → 关机 */
|
||||||
|
#define PWR_OFF_GRACE_MS 1500 /* 请求优雅关机后,超时仍未切轨则紧急断电+复位 */
|
||||||
|
|
||||||
|
|
||||||
typedef enum
|
typedef enum
|
||||||
|
|
@ -176,14 +180,44 @@ static uint32_t pwr_press_tick = 0;
|
||||||
/* 当前开关机状态(UI_Idle.c 等 extern 引用) */
|
/* 当前开关机状态(UI_Idle.c 等 extern 引用) */
|
||||||
bool powon = false;
|
bool powon = false;
|
||||||
|
|
||||||
|
/* 电源轨是否已切断(优雅关机与紧急关机共用;ISR 依此判断是否还需强切) */
|
||||||
|
static volatile uint8_t s_pwr_rails_cut = 0;
|
||||||
|
/* TMR7:关机长按已触发请求后的紧急倒计时(ms) */
|
||||||
|
static volatile uint16_t s_pwr_off_grace_ms = 0;
|
||||||
|
|
||||||
/* ==================== 开机/关机动作 ==================== */
|
/* ==================== 开机/关机动作 ==================== */
|
||||||
|
|
||||||
|
/* 仅 GPIO/背光:可在 ISR / HardFault 中调用,禁止 RTOS/Flash/日志 */
|
||||||
|
void PowerOff_CutRails(void)
|
||||||
|
{
|
||||||
|
s_pwr_rails_cut = 1u;
|
||||||
|
powon = false;
|
||||||
|
LCD_BLK_Clr();
|
||||||
|
BSP_MainPowerEnable(0);
|
||||||
|
BSP_DreamCorePowerEnable(0);
|
||||||
|
BSP_HT7178PowerEnable(0);
|
||||||
|
BSP_BlueToothPowerEnable(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PowerOff_EmergencyReset(void)
|
||||||
|
{
|
||||||
|
PowerOff_CutRails();
|
||||||
|
/* 主循环已死时无法画充电页:复位后按 USB 状态进充电/待机 */
|
||||||
|
nvic_system_reset();
|
||||||
|
}
|
||||||
|
|
||||||
void System_PowerOn(void)
|
void System_PowerOn(void)
|
||||||
{
|
{
|
||||||
LOG_I("PWR", "power_on begin");
|
LOG_I("PWR", "power_on begin");
|
||||||
|
s_pwr_rails_cut = 0u;
|
||||||
|
s_pwr_off_grace_ms = 0u;
|
||||||
|
/* 本次长按已用于开机:先消耗,再置 powon,防止 TMR7 ISR 把同一按压力即判为关机 */
|
||||||
|
Power_Key_ConsumePress();
|
||||||
/* 控制电源硬件;蓝牙按 NVM/系统设置开关恢复,勿强制常开 */
|
/* 控制电源硬件;蓝牙按 NVM/系统设置开关恢复,勿强制常开 */
|
||||||
BSP_MainPowerEnable(1);
|
BSP_MainPowerEnable(1);
|
||||||
BSP_DreamCorePowerEnable(1);
|
BSP_DreamCorePowerEnable(1);
|
||||||
BSP_HT7178PowerEnable(1);
|
BSP_HT7178PowerEnable(1);
|
||||||
|
/* 蓝牙按 NVM/系统设置开关恢复,勿强制常开 */
|
||||||
BSP_BlueToothPowerEnable((uint8_t)(mGuiData[GUI_BL_SW].Current ? 1 : 0));
|
BSP_BlueToothPowerEnable((uint8_t)(mGuiData[GUI_BL_SW].Current ? 1 : 0));
|
||||||
powon = true; /* 更新开机标志 */
|
powon = true; /* 更新开机标志 */
|
||||||
|
|
||||||
|
|
@ -196,7 +230,9 @@ void System_PowerOn(void)
|
||||||
app_tm1617_init();
|
app_tm1617_init();
|
||||||
XPT2046_Init();
|
XPT2046_Init();
|
||||||
App_Auto_Init();
|
App_Auto_Init();
|
||||||
LOG_I("PWR", "periph ready");
|
/* App 协议走 UART4:须在开机后即解析,不能等到触摸选模式才 StartTask */
|
||||||
|
StartTask();
|
||||||
|
LOG_I("PWR", "periph ready + BT UART recv started");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void PowerOn(void)
|
static void PowerOn(void)
|
||||||
|
|
@ -210,6 +246,7 @@ static void PowerOff(void)
|
||||||
|
|
||||||
LOG_I("PWR", "power_off begin");
|
LOG_I("PWR", "power_off begin");
|
||||||
powon = false; /* 更新关机标志 */
|
powon = false; /* 更新关机标志 */
|
||||||
|
s_pwr_off_grace_ms = 0u; /* 主路径已接管,取消紧急倒计时 */
|
||||||
StopFullTask();
|
StopFullTask();
|
||||||
|
|
||||||
/* 关机前把当前调音台推子与蓝牙开关写入 NVM,保证再次开机保持 */
|
/* 关机前把当前调音台推子与蓝牙开关写入 NVM,保证再次开机保持 */
|
||||||
|
|
@ -226,20 +263,19 @@ 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 保持运行以便长按开机 / 关机充电画面 */
|
/* BT 先留窗口转发已排队 ACK,再切全部电源轨 */
|
||||||
BSP_MainPowerEnable(0);
|
rt_thread_mdelay(30);
|
||||||
BSP_DreamCorePowerEnable(0);
|
PowerOff_CutRails();
|
||||||
BSP_HT7178PowerEnable(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 Image(POWOFF_CHARG) */
|
||||||
{
|
{
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
for (i = 0; i < 4; i++)
|
for (i = 0; i < 4; i++)
|
||||||
|
|
@ -253,12 +289,117 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* App remote power-off request (BLE cmd 05 00): executed in main-loop context,
|
||||||
|
so StopFullTask() never detaches the calling BT thread itself. */
|
||||||
|
static volatile uint8_t s_sys_poweroff_req = 0;
|
||||||
|
void System_RequestPowerOff(void)
|
||||||
|
{
|
||||||
|
s_sys_poweroff_req = 1;
|
||||||
|
/* 主循环若已卡死:TMR7 紧急倒计时仍会强切电源 */
|
||||||
|
if (s_pwr_off_grace_ms == 0u && !s_pwr_rails_cut)
|
||||||
|
s_pwr_off_grace_ms = PWR_OFF_GRACE_MS;
|
||||||
|
}
|
||||||
|
void System_PowerOff_Poll(void)
|
||||||
|
{
|
||||||
|
if (s_sys_poweroff_req)
|
||||||
|
{
|
||||||
|
s_sys_poweroff_req = 0;
|
||||||
|
if (powon)
|
||||||
|
PowerOff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TMR7 1ms(优先级高于伴奏 TMR6):与主循环解耦的关机键扫描。
|
||||||
|
* 正常:请求优雅关机;超时未切轨 → 紧急断电并复位。
|
||||||
|
*/
|
||||||
|
static uint16_t s_isr_hold_ms = 0;
|
||||||
|
static uint8_t s_isr_fired = 0;
|
||||||
|
|
||||||
|
/* 开机时消耗本次按键:同一次长按已用于开机,须松开后重新长按 2s 才关机。
|
||||||
|
* 须在 powon 置 true 前调用,避免 ISR 在开机瞬间把本次长按误判为关机。 */
|
||||||
|
void Power_Key_ConsumePress(void)
|
||||||
|
{
|
||||||
|
s_isr_fired = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Power_Key_IsrTick(void)
|
||||||
|
{
|
||||||
|
flag_status st;
|
||||||
|
|
||||||
|
/* RTT/USB 烧 ExtFlash 期间禁止软关机,避免长按/粘键打断 FLASH_ACK */
|
||||||
|
if (app_log_flash_busy())
|
||||||
|
return;
|
||||||
|
|
||||||
|
st = BSP_GetPowerKey();
|
||||||
|
|
||||||
|
if (st == KEY_PRESS_LEVEL)
|
||||||
|
{
|
||||||
|
if (s_isr_hold_ms < 60000u)
|
||||||
|
s_isr_hold_ms++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
s_isr_hold_ms = 0;
|
||||||
|
s_isr_fired = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 仅处理「开机态关机」;开机仍由主循环负责(需完整外设初始化) */
|
||||||
|
if (powon && !s_isr_fired && s_isr_hold_ms >= PWR_OFF_HOLD_MS)
|
||||||
|
{
|
||||||
|
s_isr_fired = 1;
|
||||||
|
System_RequestPowerOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_pwr_off_grace_ms > 0u)
|
||||||
|
{
|
||||||
|
s_pwr_off_grace_ms--;
|
||||||
|
if (s_pwr_off_grace_ms == 0u && !s_pwr_rails_cut)
|
||||||
|
PowerOff_EmergencyReset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HardFault / MemManage / BusFault / UsageFault:死循环中仍可长按关机 */
|
||||||
|
void Power_Key_FaultLoop(void)
|
||||||
|
{
|
||||||
|
uint32_t hold = 0;
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
if (BSP_GetPowerKey() == KEY_PRESS_LEVEL)
|
||||||
|
{
|
||||||
|
hold++;
|
||||||
|
if (hold >= PWR_OFF_HOLD_MS)
|
||||||
|
{
|
||||||
|
PowerOff_EmergencyReset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hold = 0;
|
||||||
|
}
|
||||||
|
Delay_us(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static rt_err_t Power_HardFault_Hook(void *context)
|
||||||
|
{
|
||||||
|
(void)context;
|
||||||
|
Power_Key_FaultLoop();
|
||||||
|
return -RT_ERROR; /* 不可达 */
|
||||||
|
}
|
||||||
|
|
||||||
|
void Power_Key_WatchdogInit(void)
|
||||||
|
{
|
||||||
|
rt_hw_exception_install(Power_HardFault_Hook);
|
||||||
|
wk_tmr7_pwrkey_init();
|
||||||
|
}
|
||||||
|
|
||||||
void Power_Key_Scan()
|
void Power_Key_Scan()
|
||||||
{
|
{
|
||||||
flag_status status = BSP_GetPowerKey();
|
flag_status status = BSP_GetPowerKey();
|
||||||
|
|
@ -317,19 +458,13 @@ void Power_Key_Scan()
|
||||||
}
|
}
|
||||||
holdtick = now - pwr_press_tick;
|
holdtick = now - pwr_press_tick;
|
||||||
|
|
||||||
/* 用 powon 区分:关机长按 3s 开机,开机长按 2s 关机 */
|
/* 关机长按 3s 开机;开机态关机改由 TMR7 ISR(主循环卡死仍可关) */
|
||||||
if(powon == false && holdtick >= PWR_ON_HOLD_MS)
|
if(powon == false && holdtick >= PWR_ON_HOLD_MS)
|
||||||
{
|
{
|
||||||
LOG_I("PWR", "key power_on hold=%u", (unsigned)holdtick);
|
LOG_I("PWR", "key power_on hold=%u", (unsigned)holdtick);
|
||||||
PowerOn();
|
PowerOn();
|
||||||
pwr_key_state = KEY_STATE_NONE;
|
pwr_key_state = KEY_STATE_NONE;
|
||||||
}
|
}
|
||||||
else if(powon == true && holdtick >= PWR_OFF_HOLD_MS)
|
|
||||||
{
|
|
||||||
LOG_I("PWR", "key power_off hold=%u", (unsigned)holdtick);
|
|
||||||
PowerOff();
|
|
||||||
pwr_key_state = KEY_STATE_NONE;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KEY_STATE_NONE:
|
case KEY_STATE_NONE:
|
||||||
|
|
@ -437,6 +572,16 @@ void Send_volume(uint8_t vol)
|
||||||
SendMidiDataToDreamDSP(mMasterSysExVolume,12);
|
SendMidiDataToDreamDSP(mMasterSysExVolume,12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 重发当前主音量:开机补发 / 进模式主页 / 开始播放前调用。
|
||||||
|
* Dream 上电早期可能丢 SysEx,丢失后旋钮不动就不再重发,
|
||||||
|
* 会出现"图标低音量、实际音量大";在关键时机重发可自愈 */
|
||||||
|
void MasterVolume_Reapply(void)
|
||||||
|
{
|
||||||
|
uint8_t vol = app_adc_get_volume();
|
||||||
|
app_adc_sync_volume(vol); /* 兜底刷新 level,保证图标与下发值一致 */
|
||||||
|
Send_volume(vol);
|
||||||
|
}
|
||||||
|
|
||||||
void Accomp_UpdatePlayingChord(void)
|
void Accomp_UpdatePlayingChord(void)
|
||||||
{
|
{
|
||||||
uint8_t midi_note_buff[4] = {0,};
|
uint8_t midi_note_buff[4] = {0,};
|
||||||
|
|
@ -455,6 +600,29 @@ void Accomp_UpdatePlayingChord(void)
|
||||||
AutoBandTop1_Note_On(midi_note_buff[i], 0x50);
|
AutoBandTop1_Note_On(midi_note_buff[i], 0x50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 尾奏起奏:Postamble(0) + 同步起奏 + 当前和弦 Note_On。
|
||||||
|
* 供 ADC 独立尾奏键与万能模式第4段落键共用。
|
||||||
|
* 万能走 piece 循环,须先退出 piece,否则 Postamble 舞台切不过去;
|
||||||
|
* 尾奏 MIDI 须由音师写入 3.bin(见 Doc/音师需求_万能3.bin尾奏_20260914.md)。 */
|
||||||
|
void AutoBand_StartOutro(void)
|
||||||
|
{
|
||||||
|
uint8_t midi_note_buff[4] = {0,};
|
||||||
|
uint8_t note_cnt = 0;
|
||||||
|
|
||||||
|
/* 退出和弦走向 piece,再进风格尾奏舞台 */
|
||||||
|
AutoBandTop1_SetPiecePlayMode(0, 0, 0);
|
||||||
|
AutoBandTop1_Postamble(0);
|
||||||
|
/* 尾奏起奏前补发主音量,保证与图标一致 */
|
||||||
|
MasterVolume_Reapply();
|
||||||
|
AutoBandTop1_SyncStart();
|
||||||
|
wk_delay_ms(50);
|
||||||
|
note_cnt = GetChordNotesByType(KEY_ID_1629,chord_type_index_map[KEY_ID_1629].type,midi_note_buff);
|
||||||
|
for(uint8_t i = 0;i < note_cnt;i++)
|
||||||
|
{
|
||||||
|
AutoBandTop1_Note_On(midi_note_buff[i],0x50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ADC_IN1_KEY_Handle(uint8_t key,bool on)
|
void ADC_IN1_KEY_Handle(uint8_t key,bool on)
|
||||||
{
|
{
|
||||||
uint8_t midi_note_buff[4] = {0,};
|
uint8_t midi_note_buff[4] = {0,};
|
||||||
|
|
@ -464,26 +632,35 @@ void ADC_IN1_KEY_Handle(uint8_t key,bool on)
|
||||||
if(key_toggle[key] == 1)
|
if(key_toggle[key] == 1)
|
||||||
{
|
{
|
||||||
//ResetAutoPowerCount();
|
//ResetAutoPowerCount();
|
||||||
switch(key)
|
if(key == 1)
|
||||||
{
|
{
|
||||||
case 3: AutoBandTop1_Preamble(0); is_preamble = 1; break;
|
/* 尾奏:与万能模式第4键共用同一起奏路径 */
|
||||||
case 0: AutoBandTop1_Preamble(1); is_preamble = 1; break;
|
AutoBand_StartOutro();
|
||||||
case 1: AutoBandTop1_Postamble(0); break;
|
|
||||||
case 2: AutoBandTop1_Postamble(1); break;
|
|
||||||
}
|
}
|
||||||
//AutoBandTop1_Start();
|
else
|
||||||
AutoBandTop1_SyncStart();
|
|
||||||
wk_delay_ms(50);
|
|
||||||
/* 音师:前奏起始 = 当前调一级和弦第一把位(index 1 + maj,再经移调) */
|
|
||||||
if (is_preamble)
|
|
||||||
{
|
{
|
||||||
KEY_ID_1629 = 1;
|
switch(key)
|
||||||
chord_type_index_map[1].type = 0;
|
{
|
||||||
}
|
case 3: AutoBandTop1_Preamble(0); is_preamble = 1; break;
|
||||||
note_cnt = GetChordNotesByType(KEY_ID_1629,chord_type_index_map[KEY_ID_1629].type,midi_note_buff);
|
case 0: AutoBandTop1_Preamble(1); is_preamble = 1; break;
|
||||||
for(uint8_t i = 0;i < note_cnt;i++)
|
case 2: AutoBandTop1_Postamble(1); break;
|
||||||
{
|
}
|
||||||
AutoBandTop1_Note_On(midi_note_buff[i],0x50);
|
/* 前奏/断奏起奏前补发主音量,保证与图标一致 */
|
||||||
|
MasterVolume_Reapply();
|
||||||
|
//AutoBandTop1_Start();
|
||||||
|
AutoBandTop1_SyncStart();
|
||||||
|
wk_delay_ms(50);
|
||||||
|
/* 音师:前奏起始 = 当前调一级和弦第一把位(index 1 + maj,再经移调) */
|
||||||
|
if (is_preamble)
|
||||||
|
{
|
||||||
|
KEY_ID_1629 = 1;
|
||||||
|
chord_type_index_map[1].type = 0;
|
||||||
|
}
|
||||||
|
note_cnt = GetChordNotesByType(KEY_ID_1629,chord_type_index_map[KEY_ID_1629].type,midi_note_buff);
|
||||||
|
for(uint8_t i = 0;i < note_cnt;i++)
|
||||||
|
{
|
||||||
|
AutoBandTop1_Note_On(midi_note_buff[i],0x50);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -610,18 +787,100 @@ uint8_t BP_Transpose(uint8_t key_idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 伴奏活动跟踪:AutoBand PlayTick 前进即视为播放中。
|
||||||
|
* 供专业模式指板键区分「播放中换和弦」与「段落结束后续奏重启」。 */
|
||||||
|
static uint64_t s_play_tick_last = 0;
|
||||||
|
static uint32_t s_play_adv_tick = 0; /* 最近一次 PlayTick 前进的系统 tick */
|
||||||
|
|
||||||
|
void Band_TrackPlayTick(void)
|
||||||
|
{
|
||||||
|
uint64_t pt;
|
||||||
|
|
||||||
|
if (!StartFlag)
|
||||||
|
{
|
||||||
|
s_play_tick_last = 0;
|
||||||
|
s_play_adv_tick = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pt = AutoBandTop1_GetCurrentPlayTick();
|
||||||
|
if (pt != s_play_tick_last)
|
||||||
|
{
|
||||||
|
s_play_tick_last = pt;
|
||||||
|
s_play_adv_tick = rt_tick_get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Band_IsPlaying(void)
|
||||||
|
{
|
||||||
|
if (!StartFlag || s_play_adv_tick == 0)
|
||||||
|
return 0;
|
||||||
|
return (rt_tick_get() - s_play_adv_tick) < (RT_TICK_PER_SECOND / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 专业模式公共起奏:拨片首次起奏与起奏后的指板续奏共用。
|
||||||
|
* 节奏库走 piece 循环;本地曲目按当前段落走 StartWithNote。 */
|
||||||
|
static void Expert_StartWithChord(uint8_t chord_id, uint8_t *midi_note_buff,
|
||||||
|
uint8_t note_cnt, const char *trigger)
|
||||||
|
{
|
||||||
|
uint8_t midi = 0;
|
||||||
|
|
||||||
|
StartFlag = 1;
|
||||||
|
if (mGuiData[GUI_AUTOBAND_SW].Current)
|
||||||
|
{
|
||||||
|
AutoBandTop1_Stop();
|
||||||
|
AutoBandTop1_SetLoopMode(0, 1);
|
||||||
|
AutoBandTop1_SetRunVar(mGuiData[GUI_PLAY_SECTION].Current);
|
||||||
|
midi = 0x3C + mGuiData[GUI_TRANSPOSE].Current;
|
||||||
|
AutoBandTop1_StartWithNote(midi);
|
||||||
|
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||||
|
LOG_I("pick", "local/expert root=%u name=%s sw=%u tab=%u trigger=%s chord=%u",
|
||||||
|
midi,
|
||||||
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
||||||
|
(unsigned)mGuiData[GUI_AUTOBAND_SW].Current,
|
||||||
|
(unsigned)mGuiData[GUI_TAB_INDEX].Current,
|
||||||
|
trigger,
|
||||||
|
(unsigned)chord_id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AutoBandTop1_SetPiecePlayMode(1, 0, 1920);
|
||||||
|
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||||
|
AutoBandTop1_SetLoopMode(0, 1);
|
||||||
|
LOG_I("pick", "expert/rhythm piece name=%s trigger=%s chord=%u",
|
||||||
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
||||||
|
trigger,
|
||||||
|
(unsigned)chord_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void TM1629_Handle(uint8_t key)
|
void TM1629_Handle(uint8_t key)
|
||||||
{
|
{
|
||||||
STRING_MIDI *MIDI;
|
STRING_MIDI *MIDI;
|
||||||
const STRING_MIDI *ORGAN_MIDI;
|
const STRING_MIDI *ORGAN_MIDI;
|
||||||
static uint8_t Last_Tm1926D_Key = 0;
|
|
||||||
|
/* 万能:指板不改和弦、不启停伴奏;保留拍速/停止 */
|
||||||
|
if (mGuiData[GUI_TAB_INDEX].Current == 0)
|
||||||
|
{
|
||||||
|
if (key == 22) { Tap_to_Speed(); return; }
|
||||||
|
if (key == 23) { Stop_AutoBand(); return; }
|
||||||
|
return; /* 忽略 0~21:不改 KEY_ID/USE_MIDI/PressFlag,不启停 OutTime_Stop */
|
||||||
|
}
|
||||||
|
|
||||||
if(BP_Transpose(key)) return;
|
if(BP_Transpose(key)) return;
|
||||||
|
|
||||||
switch(key)
|
switch(key)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
PressFlag = 0;
|
PressFlag = 0;
|
||||||
if(/*StartFlag &&*/ mGuiData[GUI_TAB_INDEX].Current == 0)
|
if(mGuiData[GUI_TAB_INDEX].Current == 2)
|
||||||
|
{
|
||||||
|
/* 普通:松手立即结束当前和弦伴奏 */
|
||||||
|
KEY_ID_1629 = 0;
|
||||||
|
if (StartFlag)
|
||||||
|
Stop_AutoBand();
|
||||||
|
}
|
||||||
|
else if(mGuiData[GUI_TAB_INDEX].Current == 0)
|
||||||
{
|
{
|
||||||
//StartFlag = 0;
|
//StartFlag = 0;
|
||||||
//tick_ever_positive = false;
|
//tick_ever_positive = false;
|
||||||
|
|
@ -640,6 +899,11 @@ void TM1629_Handle(uint8_t key)
|
||||||
rt_timer_stop(OutTime_Stop_timer);
|
rt_timer_stop(OutTime_Stop_timer);
|
||||||
//osTimerStop(Delay_stop_timer);
|
//osTimerStop(Delay_stop_timer);
|
||||||
}
|
}
|
||||||
|
else if(mGuiData[GUI_TAB_INDEX].Current == 2)
|
||||||
|
{
|
||||||
|
/* 普通:按住指板,等待拨片起奏当前和弦 */
|
||||||
|
PressFlag = 1;
|
||||||
|
}
|
||||||
uint8_t group = (key - 1) / 3; /* 1~3→0, 4~6→1, ..., 19~21→6 */
|
uint8_t group = (key - 1) / 3; /* 1~3→0, 4~6→1, ..., 19~21→6 */
|
||||||
KEY_ID_1629 = key;
|
KEY_ID_1629 = key;
|
||||||
MIDI = (STRING_MIDI *)Chord_Midi_Table[group];
|
MIDI = (STRING_MIDI *)Chord_Midi_Table[group];
|
||||||
|
|
@ -672,18 +936,30 @@ void TM1629_Handle(uint8_t key)
|
||||||
USE_MIDI.Midi_5 += mGuiData[GUI_TRANSPOSE].Current;
|
USE_MIDI.Midi_5 += mGuiData[GUI_TRANSPOSE].Current;
|
||||||
USE_MIDI.Midi_6 += mGuiData[GUI_TRANSPOSE].Current;
|
USE_MIDI.Midi_6 += mGuiData[GUI_TRANSPOSE].Current;
|
||||||
|
|
||||||
if(mGuiData[GUI_TAB_INDEX].Current == 1 && StartFlag && Last_Tm1926D_Key != KEY_ID_1629)
|
if(mGuiData[GUI_TAB_INDEX].Current == 1 && StartFlag)
|
||||||
{
|
{
|
||||||
uint8_t midi_note_buff[4] = {0,};
|
/* 不按 Last_Tm1926D_Key 去重:App 曲目相邻两节可能是同一和弦键,
|
||||||
uint8_t note_cnt = 0;
|
* 松手再按同一键也必须能续奏(与「指板+拨片」一致,拨片本无去重) */
|
||||||
Last_Tm1926D_Key = KEY_ID_1629;
|
if (mGuiData[GUI_AUTOBAND_SW].Current || !Band_IsPlaying())
|
||||||
//AutoBandTop1_SetPiecePlayMode(1,0,480);
|
|
||||||
//AutoBandTop1_StartWithNotes(midi_note_buff,GetChordNotesByType(KEY_ID_1629,chord_type_index_map[KEY_ID_1629].type,midi_note_buff));
|
|
||||||
//AutoBandTop1_SetLoopMode(0,1);
|
|
||||||
note_cnt = GetChordNotesByType(KEY_ID_1629,chord_type_index_map[KEY_ID_1629].type,midi_note_buff);
|
|
||||||
for(uint8_t i = 0;i < note_cnt;i++)
|
|
||||||
{
|
{
|
||||||
AutoBandTop1_Note_On(midi_note_buff[i],0x50);
|
/* 本地曲目(App 和弦曲目):段落结束/循环点处仅按指板即续奏下一节。
|
||||||
|
* 转主任务走 Pick_Handle,与「指板+拨片」完全等价——
|
||||||
|
* 扫描线程内直接 Stop+Start 重启会无声,主任务上下文则正常。
|
||||||
|
* Pick_Handle 内会调 Return_Light_buff 通知 App 亮下一节。 */
|
||||||
|
MainTask_Sendmsg(MSG_ID_BP_KEY, 0, 0, 0);
|
||||||
|
LOG_I("pick", "expert/fret continue chord=%u", (unsigned)KEY_ID_1629);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* 节奏库播放中:仅换和弦音,不重启段落(说明书:左手切换和弦) */
|
||||||
|
uint8_t midi_note_buff[4] = {0,};
|
||||||
|
uint8_t note_cnt = GetChordNotesByType(KEY_ID_1629,chord_type_index_map[KEY_ID_1629].type,midi_note_buff);
|
||||||
|
for(uint8_t i = 0;i < note_cnt;i++)
|
||||||
|
{
|
||||||
|
AutoBandTop1_Note_On(midi_note_buff[i],0x50);
|
||||||
|
}
|
||||||
|
Return_Light_buff(KEY_ID_1629);
|
||||||
|
LOG_I("pick", "expert/fret change chord=%u", (unsigned)KEY_ID_1629);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -728,8 +1004,19 @@ void Pick_Handle()
|
||||||
uint8_t note_cnt = 0;
|
uint8_t note_cnt = 0;
|
||||||
uint8_t chord_id = KEY_ID_1629;
|
uint8_t chord_id = KEY_ID_1629;
|
||||||
|
|
||||||
/* 左手未触发和弦板(或落在拍速/停止键)时:拨片默认发当前调一级大和弦伴奏 */
|
/* 空库/加载失败时禁止起奏:否则 TMR6 ISR 内 AutoBand 可能 HardFault,
|
||||||
if (chord_id == 0 || chord_id == 22 || chord_id == 23)
|
* LCD 卡死且主循环 Power_Key_Scan 不再跑 → 只能 RESET */
|
||||||
|
if (!App_Auto_IsPresetReady())
|
||||||
|
{
|
||||||
|
LOG_E("pick", "abort: preset not ready tab=%u addr=0x%08X",
|
||||||
|
(unsigned)mGuiData[GUI_TAB_INDEX].Current, (unsigned)ADDRESS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左手未触发和弦板(或落在拍速/停止键)时:拨片默认发当前调一级大和弦伴奏;
|
||||||
|
* 普通模式除外——必须左手按住指板再拨,不做一级兜底(说明书需求) */
|
||||||
|
if (mGuiData[GUI_TAB_INDEX].Current != 2 &&
|
||||||
|
(chord_id == 0 || chord_id == 22 || chord_id == 23))
|
||||||
{
|
{
|
||||||
KEY_ID_1629 = 1;
|
KEY_ID_1629 = 1;
|
||||||
chord_type_index_map[1].type = 0;
|
chord_type_index_map[1].type = 0;
|
||||||
|
|
@ -738,6 +1025,10 @@ void Pick_Handle()
|
||||||
note_cnt = GetChordNotesByType(chord_id, chord_type_index_map[chord_id].type, midi_note_buff);
|
note_cnt = GetChordNotesByType(chord_id, chord_type_index_map[chord_id].type, midi_note_buff);
|
||||||
Return_Light_buff(KEY_ID_1629);
|
Return_Light_buff(KEY_ID_1629);
|
||||||
|
|
||||||
|
/* 首次起奏前补发主音量:防开机 SysEx 丢失后"图标低、实际大" */
|
||||||
|
if (StartFlag == 0)
|
||||||
|
MasterVolume_Reapply();
|
||||||
|
|
||||||
switch(mGuiData[GUI_TAB_INDEX].Current)
|
switch(mGuiData[GUI_TAB_INDEX].Current)
|
||||||
{
|
{
|
||||||
case 3:
|
case 3:
|
||||||
|
|
@ -760,76 +1051,60 @@ void Pick_Handle()
|
||||||
AutoBandTop1_SetLoopMode(0,1);
|
AutoBandTop1_SetLoopMode(0,1);
|
||||||
break;
|
break;
|
||||||
case 0:
|
case 0:
|
||||||
if(PressFlag)
|
/* 设置页内沿用万能:与主路径一致,始终 loop */
|
||||||
{
|
AutoBandTop1_SetPiecePlayMode(1, 0, 480);
|
||||||
AutoBandTop1_SetLoopMode(0,1);
|
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
AutoBandTop1_SetLoopMode(0, 1);
|
||||||
}else
|
|
||||||
{
|
|
||||||
AutoBandTop1_SetPiecePlayMode(1,0,480);
|
|
||||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
|
||||||
}
|
|
||||||
StartFlag = 1;
|
StartFlag = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
/* 普通:节奏/本地曲目均用 StartWithNote;切库后允许再次拨片起奏 */
|
/* 普通:左手按住指板 + 拨片起奏;松手由 TM1629 停。
|
||||||
if (StartFlag == 0 || mGuiData[GUI_AUTOBAND_SW].Current)
|
* 节奏用 piece;本地曲目用 StartWithNote */
|
||||||
|
if (PressFlag == 0 || chord_id == 0 || chord_id > 21)
|
||||||
|
{
|
||||||
|
LOG_I("pick", "normal skip press=%u chord=%u (need fretboard)",
|
||||||
|
(unsigned)PressFlag, (unsigned)chord_id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
StartFlag = 1;
|
||||||
|
if (mGuiData[GUI_AUTOBAND_SW].Current)
|
||||||
{
|
{
|
||||||
StartFlag = 1;
|
|
||||||
AutoBandTop1_Stop();
|
AutoBandTop1_Stop();
|
||||||
AutoBandTop1_SetLoopMode(0,1);
|
AutoBandTop1_SetLoopMode(0,1);
|
||||||
AutoBandTop1_SetRunVar(mGuiData[GUI_PLAY_SECTION].Current);
|
AutoBandTop1_SetRunVar(mGuiData[GUI_PLAY_SECTION].Current);
|
||||||
/* 起调必须用 0x3C+移调;勿用和弦音区 MIDI(如 48),否则本地曲目起奏异常 */
|
|
||||||
midi = 0x3C + mGuiData[GUI_TRANSPOSE].Current;
|
midi = 0x3C + mGuiData[GUI_TRANSPOSE].Current;
|
||||||
AutoBandTop1_StartWithNote(midi);
|
AutoBandTop1_StartWithNote(midi);
|
||||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||||
if (mGuiData[GUI_AUTOBAND_SW].Current)
|
LOG_I("pick", "local/normal root=%u name=%s", midi,
|
||||||
LOG_I("pick", "local/normal root=%u name=%s", midi,
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?");
|
||||||
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?");
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AutoBandTop1_SetPiecePlayMode(1, 0, 1920);
|
||||||
|
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||||
|
AutoBandTop1_SetLoopMode(0, 1);
|
||||||
|
LOG_I("pick", "normal/rhythm piece name=%s notes=%u chord=%u",
|
||||||
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
||||||
|
(unsigned)note_cnt, (unsigned)chord_id);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
StartFlag = 1;
|
/* 首次拨片起奏与后续指板续奏共用同一专业起奏路径 */
|
||||||
if (mGuiData[GUI_AUTOBAND_SW].Current)
|
Expert_StartWithChord(chord_id, midi_note_buff, note_cnt, "pick");
|
||||||
{
|
|
||||||
/* 本地曲目:与普通模式同一套起奏(0x3C 根音 + 和弦音) */
|
|
||||||
AutoBandTop1_Stop();
|
|
||||||
AutoBandTop1_SetLoopMode(0,1);
|
|
||||||
AutoBandTop1_SetRunVar(mGuiData[GUI_PLAY_SECTION].Current);
|
|
||||||
midi = 0x3C + mGuiData[GUI_TRANSPOSE].Current;
|
|
||||||
AutoBandTop1_StartWithNote(midi);
|
|
||||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
|
||||||
LOG_I("pick", "local/expert root=%u name=%s sw=%u tab=%u",
|
|
||||||
midi,
|
|
||||||
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
|
||||||
(unsigned)mGuiData[GUI_AUTOBAND_SW].Current,
|
|
||||||
(unsigned)mGuiData[GUI_TAB_INDEX].Current);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AutoBandTop1_SetPiecePlayMode(1,0,1920);
|
|
||||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
|
||||||
AutoBandTop1_SetLoopMode(0,1);
|
|
||||||
LOG_I("pick", "expert/rhythm piece name=%s",
|
|
||||||
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?");
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 0:
|
case 0:
|
||||||
|
/* 万能:和弦走向用 piece 模式;须 SetLoopMode,否则无循环时常听不到伴奏 */
|
||||||
StartFlag = 1;
|
StartFlag = 1;
|
||||||
if(PressFlag)
|
AutoBandTop1_SetPiecePlayMode(1, 0, 480);
|
||||||
{
|
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||||
AutoBandTop1_SetPiecePlayMode(1,0,480);
|
AutoBandTop1_SetLoopMode(0, 1);
|
||||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
LOG_I("pick", "universal piece name=%s press=%u notes=%u chord=%u",
|
||||||
AutoBandTop1_SetLoopMode(0,1);
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
||||||
}else
|
(unsigned)PressFlag, (unsigned)note_cnt, (unsigned)chord_id);
|
||||||
{
|
|
||||||
AutoBandTop1_SetPiecePlayMode(1,0,480);
|
|
||||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -941,9 +1216,8 @@ void BL_Set_led(uint8_t led, uint8_t chord_type)
|
||||||
|
|
||||||
void BT_Pitch_offset_map(uint8_t pos_offset,uint8_t pitch_offset)
|
void BT_Pitch_offset_map(uint8_t pos_offset,uint8_t pitch_offset)
|
||||||
{
|
{
|
||||||
//char Info[10];
|
if (pos_offset < 1u || pos_offset > 21u)
|
||||||
//sprintf (Info, "%d %d", pos_offset,pitch_offset);
|
return;
|
||||||
//LCD_ShowString(2, 156+16, (const uint8_t*)Info, RED, WHITE, 16, 0);
|
|
||||||
chord_type_index_map[pos_offset].PitchOffset = pitch_offset;
|
chord_type_index_map[pos_offset].PitchOffset = pitch_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -952,10 +1226,13 @@ void BT_Chord_offset_map(uint8_t pos_offset,uint8_t chord_offset)
|
||||||
//char Info[20];
|
//char Info[20];
|
||||||
STRING_MIDI *MIDI;
|
STRING_MIDI *MIDI;
|
||||||
const STRING_MIDI *ORGAN_MIDI;
|
const STRING_MIDI *ORGAN_MIDI;
|
||||||
|
uint8_t group;
|
||||||
uint8_t group = (pos_offset - 1) / 3; /* 1~3→0, 4~6→1, ..., 19~21→6 */
|
|
||||||
uint8_t chord_APP_Local_offset = 0;
|
uint8_t chord_APP_Local_offset = 0;
|
||||||
|
|
||||||
|
if (pos_offset < 1u || pos_offset > 21u)
|
||||||
|
return;
|
||||||
|
group = (pos_offset - 1) / 3; /* 1~3→0, 4~6→1, ..., 19~21→6 */
|
||||||
|
|
||||||
switch(chord_offset){
|
switch(chord_offset){
|
||||||
case 0x00:
|
case 0x00:
|
||||||
chord_APP_Local_offset = 0;
|
chord_APP_Local_offset = 0;
|
||||||
|
|
@ -1280,9 +1557,10 @@ uint8_t GetChordNotesByType(uint8_t MIDI_Index,uint8_t type,uint8_t *buff)
|
||||||
root = buff[0];
|
root = buff[0];
|
||||||
for (i = 1; i < n; i++)
|
for (i = 1; i < n; i++)
|
||||||
{
|
{
|
||||||
while (buff[i] >= (uint8_t)(root + 12))
|
uint8_t guard;
|
||||||
|
for (guard = 0; guard < 2 && buff[i] >= (uint8_t)(root + 12); guard++)
|
||||||
buff[i] -= 12;
|
buff[i] -= 12;
|
||||||
while (buff[i] < root)
|
for (guard = 0; guard < 2 && buff[i] < root; guard++)
|
||||||
buff[i] += 12;
|
buff[i] += 12;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1300,6 +1578,8 @@ void Read_Cur_Measure()
|
||||||
(tab == 0 || tab == 2) ||
|
(tab == 0 || tab == 2) ||
|
||||||
(tab == 3 && (last == 0 || last == 2));
|
(tab == 3 && (last == 0 || last == 2));
|
||||||
|
|
||||||
|
Band_TrackPlayTick();
|
||||||
|
|
||||||
if (loop_light_ok && StartFlag)
|
if (loop_light_ok && StartFlag)
|
||||||
{
|
{
|
||||||
if(now - last_tick_i >= 10)
|
if(now - last_tick_i >= 10)
|
||||||
|
|
|
||||||
|
|
@ -166,8 +166,15 @@ extern uint8_t KEY_ID_1629;
|
||||||
|
|
||||||
extern bool powon;
|
extern bool powon;
|
||||||
void Power_Key_Scan(void);
|
void Power_Key_Scan(void);
|
||||||
|
void Power_Key_IsrTick(void);
|
||||||
|
void Power_Key_FaultLoop(void);
|
||||||
|
void Power_Key_WatchdogInit(void);
|
||||||
|
void Power_Key_ConsumePress(void);
|
||||||
|
void PowerOff_CutRails(void);
|
||||||
void AutoPowerOff_Scan(void);
|
void AutoPowerOff_Scan(void);
|
||||||
void System_PowerOn(void);
|
void System_PowerOn(void);
|
||||||
|
void System_RequestPowerOff(void);
|
||||||
|
void System_PowerOff_Poll(void);
|
||||||
extern uint8_t Led;
|
extern uint8_t Led;
|
||||||
extern bool PressFlag;
|
extern bool PressFlag;
|
||||||
|
|
||||||
|
|
@ -211,6 +218,9 @@ uint8_t GUI_Item_AjustValue(GUI_SWITCH * Group, int8_t Dir);
|
||||||
void GUI_Item_AjustLoopValue(GUI_SWITCH * Group, int8_t Dir);
|
void GUI_Item_AjustLoopValue(GUI_SWITCH * Group, int8_t Dir);
|
||||||
|
|
||||||
void Send_volume(uint8_t vol);
|
void Send_volume(uint8_t vol);
|
||||||
|
void MasterVolume_Reapply(void);
|
||||||
|
/* 尾奏起奏:ADC 尾奏键与万能第4段落键共用 */
|
||||||
|
void AutoBand_StartOutro(void);
|
||||||
|
|
||||||
void ADC_IN1_KEY_Handle(uint8_t key,bool on);
|
void ADC_IN1_KEY_Handle(uint8_t key,bool on);
|
||||||
void TM1617_Handle(uint8_t key);
|
void TM1617_Handle(uint8_t key);
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,329 @@ static uint8_t pStoreBuffer[PRESET_BUFFER_SIZE];
|
||||||
//static uint8_t* pStoreBuffer;
|
//static uint8_t* pStoreBuffer;
|
||||||
#define DEFAULT_TEMPO 100
|
#define DEFAULT_TEMPO 100
|
||||||
|
|
||||||
|
/* ======== Bass/和弦分通道八度(无 AutoBand 源码时的输出侧映射) ========
|
||||||
|
* 代码通道(0 起,与 MIDI status 低 4 位一致):8=bass,9=鼓;其余=和弦。
|
||||||
|
* 日志打印同号(ch8=bass)。I/II(键1~6)原样;III+:bass -12 地板 E1;
|
||||||
|
* 和弦 -12 夹在 E2~#G4,超上限就近和弦内音。移调≥#F(6) 时再 -12。
|
||||||
|
* 对照 Doc/吉他和弦表最终版.xlsx */
|
||||||
|
#define CHORD_REG_FIX_EN 1
|
||||||
|
#define BASS_CH 8 /* 日志 ch8 */
|
||||||
|
#define DRUM_CH 9 /* 日志 ch9 */
|
||||||
|
#define MIDI_E1 28
|
||||||
|
#define MIDI_E2 40
|
||||||
|
#define MIDI_GSHARP4 68
|
||||||
|
#define XPOSE_FS 6 /* C=0 时 #F=6 */
|
||||||
|
|
||||||
|
/* 映射策略指纹:变化时 CC123 清旧音,防挂音(不建大音符表) */
|
||||||
|
static uint8_t s_reg_fp = 0xFFu;
|
||||||
|
/* 每个指纹周期内 NoteOn 映射日志限额(和弦与 bass 分开,避免和弦占满看不到 bass) */
|
||||||
|
static uint8_t s_pitch_log_left = 0;
|
||||||
|
static uint8_t s_bass_log_left = 0;
|
||||||
|
static uint16_t s_ch_ever_mask = 0; /* 会话内各通道是否出现过 NoteOn */
|
||||||
|
#define PITCH_LOG_PER_FP 24
|
||||||
|
#define BASS_LOG_PER_FP 16
|
||||||
|
|
||||||
|
static int App_Auto_ClampMidi(int n)
|
||||||
|
{
|
||||||
|
if (n < 0) return 0;
|
||||||
|
if (n > 127) return 127;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_ChordDegree(void)
|
||||||
|
{
|
||||||
|
if (KEY_ID_1629 < 1 || KEY_ID_1629 > 21)
|
||||||
|
return 0;
|
||||||
|
return (int)((KEY_ID_1629 - 1) / 3) + 1; /* 1=I .. 7=VII */
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_NeedRegisterFix(void)
|
||||||
|
{
|
||||||
|
/* 键 1~6 = I/II:不动;7~21 = III~VII */
|
||||||
|
return (KEY_ID_1629 >= 7 && KEY_ID_1629 <= 21) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_XposeExtra(void)
|
||||||
|
{
|
||||||
|
return (mGuiData[GUI_TRANSPOSE].Current >= XPOSE_FS) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t App_Auto_RegFingerprint(void)
|
||||||
|
{
|
||||||
|
/* bit0=need_fix, bit1=xpose_extra, bit2.. = 粗粒度级数 (key/3) */
|
||||||
|
uint8_t fp = 0;
|
||||||
|
if (App_Auto_NeedRegisterFix()) fp |= 0x01u;
|
||||||
|
if (App_Auto_XposeExtra()) fp |= 0x02u;
|
||||||
|
if (KEY_ID_1629 >= 1 && KEY_ID_1629 <= 21)
|
||||||
|
fp |= (uint8_t)(((KEY_ID_1629 - 1) / 3) << 2);
|
||||||
|
return fp;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void App_Auto_QueueAllNotesOffMelodic(void)
|
||||||
|
{
|
||||||
|
MidiFifoItem_t m;
|
||||||
|
int ch;
|
||||||
|
for (ch = 0; ch < 16; ch++)
|
||||||
|
{
|
||||||
|
if (ch == DRUM_CH) continue;
|
||||||
|
m.msg[0] = (uint8_t)(0xB0 | ch);
|
||||||
|
m.msg[1] = 123; /* All Notes Off */
|
||||||
|
m.msg[2] = 0;
|
||||||
|
m.msglen = 3;
|
||||||
|
mymidififo_InQueue(&m_MidiSendFifo, &m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 超出 #G4:在当前和弦内音(GetChordNotesByType,对照和弦表「和弦伴奏」列)中就近 */
|
||||||
|
static int App_Auto_NearestChordTone(int target)
|
||||||
|
{
|
||||||
|
uint8_t buff[4] = {0, 0, 0, 0};
|
||||||
|
uint8_t n;
|
||||||
|
uint8_t pcs[4];
|
||||||
|
int i, oct, best = -1, best_dist = 9999;
|
||||||
|
uint8_t type;
|
||||||
|
|
||||||
|
if (KEY_ID_1629 < 1 || KEY_ID_1629 > 21)
|
||||||
|
return App_Auto_ClampMidi(target);
|
||||||
|
|
||||||
|
type = chord_type_index_map[KEY_ID_1629].type;
|
||||||
|
n = GetChordNotesByType(KEY_ID_1629, type, buff);
|
||||||
|
if (n == 0)
|
||||||
|
return App_Auto_ClampMidi(target > MIDI_GSHARP4 ? MIDI_GSHARP4 : target);
|
||||||
|
|
||||||
|
for (i = 0; i < (int)n; i++)
|
||||||
|
pcs[i] = (uint8_t)(buff[i] % 12);
|
||||||
|
|
||||||
|
for (i = 0; i < (int)n; i++)
|
||||||
|
{
|
||||||
|
for (oct = 0; oct < 11; oct++)
|
||||||
|
{
|
||||||
|
int cand = (int)pcs[i] + oct * 12;
|
||||||
|
int dist;
|
||||||
|
if (cand < MIDI_E2 || cand > MIDI_GSHARP4)
|
||||||
|
continue;
|
||||||
|
dist = cand - target;
|
||||||
|
if (dist < 0) dist = -dist;
|
||||||
|
/* 并列:优先较低者(不超过上限) */
|
||||||
|
if (dist < best_dist || (dist == best_dist && (best < 0 || cand < best)))
|
||||||
|
{
|
||||||
|
best_dist = dist;
|
||||||
|
best = cand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best < 0)
|
||||||
|
return MIDI_GSHARP4;
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_MapBassNote(int key, uint8_t *flags)
|
||||||
|
{
|
||||||
|
int out = key - 12;
|
||||||
|
uint8_t f = 0x01u; /* bit0: -12 applied */
|
||||||
|
while (out < MIDI_E1)
|
||||||
|
{
|
||||||
|
out += 12;
|
||||||
|
f |= 0x02u; /* bit1: floored up to E1 */
|
||||||
|
}
|
||||||
|
if (App_Auto_XposeExtra())
|
||||||
|
{
|
||||||
|
out -= 12;
|
||||||
|
f |= 0x04u; /* bit2: xpose extra -12 */
|
||||||
|
while (out < MIDI_E1)
|
||||||
|
{
|
||||||
|
out += 12;
|
||||||
|
f |= 0x02u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flags) *flags = f;
|
||||||
|
return App_Auto_ClampMidi(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_MapChordNote(int key, uint8_t *flags)
|
||||||
|
{
|
||||||
|
int out = key - 12;
|
||||||
|
uint8_t f = 0x01u; /* bit0: -12 */
|
||||||
|
|
||||||
|
while (out < MIDI_E2)
|
||||||
|
{
|
||||||
|
out += 12;
|
||||||
|
f |= 0x08u; /* bit3: raised to E2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out > MIDI_GSHARP4)
|
||||||
|
{
|
||||||
|
out = App_Auto_NearestChordTone(out);
|
||||||
|
f |= 0x10u; /* bit4: nearest chord tone */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (App_Auto_XposeExtra())
|
||||||
|
{
|
||||||
|
out -= 12;
|
||||||
|
f |= 0x04u;
|
||||||
|
while (out < MIDI_E2)
|
||||||
|
{
|
||||||
|
out += 12;
|
||||||
|
f |= 0x08u;
|
||||||
|
}
|
||||||
|
if (out > MIDI_GSHARP4)
|
||||||
|
{
|
||||||
|
out = App_Auto_NearestChordTone(out);
|
||||||
|
f |= 0x10u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flags) *flags = f;
|
||||||
|
return App_Auto_ClampMidi(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_MapNote(int channel, int key, uint8_t *flags)
|
||||||
|
{
|
||||||
|
int out = key;
|
||||||
|
if (flags) *flags = 0;
|
||||||
|
|
||||||
|
#if CHORD_REG_FIX_EN
|
||||||
|
if (channel == DRUM_CH || key < 0 || key > 127)
|
||||||
|
return key;
|
||||||
|
|
||||||
|
if (!App_Auto_NeedRegisterFix())
|
||||||
|
return key;
|
||||||
|
|
||||||
|
if (channel == BASS_CH)
|
||||||
|
out = App_Auto_MapBassNote(key, flags);
|
||||||
|
else
|
||||||
|
out = App_Auto_MapChordNote(key, flags);
|
||||||
|
#else
|
||||||
|
(void)channel;
|
||||||
|
#endif
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *App_Auto_RoleName(int channel)
|
||||||
|
{
|
||||||
|
if (channel == DRUM_CH) return "drum";
|
||||||
|
if (channel == BASS_CH) return "bass";
|
||||||
|
return "chord";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void App_Auto_LogPitch(int channel, int key, int out, uint8_t flags, int is_on)
|
||||||
|
{
|
||||||
|
const char *role;
|
||||||
|
int deg;
|
||||||
|
int fix;
|
||||||
|
int xf;
|
||||||
|
int pass;
|
||||||
|
|
||||||
|
if (channel == DRUM_CH)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (channel == BASS_CH)
|
||||||
|
{
|
||||||
|
if (s_bass_log_left == 0)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (s_pitch_log_left == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
role = App_Auto_RoleName(channel);
|
||||||
|
deg = App_Auto_ChordDegree();
|
||||||
|
fix = App_Auto_NeedRegisterFix();
|
||||||
|
xf = App_Auto_XposeExtra();
|
||||||
|
pass = (!fix || channel == DRUM_CH) ? 1 : 0;
|
||||||
|
|
||||||
|
LOG_I("PITCH", "%s ch%u(%s) %d->%d d=%d deg=%d chord=%u xp=%u xf=%d fl=0x%02X %s%s%s%s",
|
||||||
|
is_on ? "on" : "off",
|
||||||
|
(unsigned)channel, role, /* 0 基,与 BASS_CH=8 一致 */
|
||||||
|
key, out, out - key,
|
||||||
|
deg, (unsigned)KEY_ID_1629,
|
||||||
|
(unsigned)mGuiData[GUI_TRANSPOSE].Current, xf,
|
||||||
|
(unsigned)flags,
|
||||||
|
pass ? "PASS " : "",
|
||||||
|
(flags & 0x10u) ? "NEAR " : "",
|
||||||
|
(flags & 0x02u) ? "E1UP " : "",
|
||||||
|
(flags & 0x08u) ? "E2UP " : "");
|
||||||
|
|
||||||
|
if (channel == BASS_CH)
|
||||||
|
{
|
||||||
|
if (s_bass_log_left > 0)
|
||||||
|
s_bass_log_left--;
|
||||||
|
}
|
||||||
|
else if (s_pitch_log_left > 0)
|
||||||
|
{
|
||||||
|
s_pitch_log_left--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NoteOn 路径:策略变化时清旧音;返回映射后音高 */
|
||||||
|
static int App_Auto_MapNoteOn(int channel, int key)
|
||||||
|
{
|
||||||
|
#if CHORD_REG_FIX_EN
|
||||||
|
uint8_t fp = App_Auto_RegFingerprint();
|
||||||
|
uint8_t flags = 0;
|
||||||
|
int out;
|
||||||
|
|
||||||
|
if (fp != s_reg_fp)
|
||||||
|
{
|
||||||
|
if (s_reg_fp != 0xFFu)
|
||||||
|
{
|
||||||
|
LOG_I("REG", "fp %u->%u chord=%u deg=%d xp=%u xf=%d",
|
||||||
|
(unsigned)s_reg_fp, (unsigned)fp,
|
||||||
|
(unsigned)KEY_ID_1629,
|
||||||
|
App_Auto_ChordDegree(),
|
||||||
|
(unsigned)mGuiData[GUI_TRANSPOSE].Current,
|
||||||
|
App_Auto_XposeExtra());
|
||||||
|
App_Auto_QueueAllNotesOffMelodic();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG_I("REG", "fp init %u chord=%u deg=%d xp=%u",
|
||||||
|
(unsigned)fp, (unsigned)KEY_ID_1629,
|
||||||
|
App_Auto_ChordDegree(),
|
||||||
|
(unsigned)mGuiData[GUI_TRANSPOSE].Current);
|
||||||
|
}
|
||||||
|
s_reg_fp = fp;
|
||||||
|
s_pitch_log_left = PITCH_LOG_PER_FP;
|
||||||
|
s_bass_log_left = BASS_LOG_PER_FP;
|
||||||
|
s_ch_ever_mask = 0; /* 新策略周期重新报到通道 */
|
||||||
|
}
|
||||||
|
|
||||||
|
out = App_Auto_MapNote(channel, key, &flags);
|
||||||
|
if (channel >= 0 && channel < 16)
|
||||||
|
{
|
||||||
|
uint16_t bit = (uint16_t)(1u << channel);
|
||||||
|
if ((s_ch_ever_mask & bit) == 0u)
|
||||||
|
{
|
||||||
|
s_ch_ever_mask |= bit;
|
||||||
|
LOG_I("REG", "ch-seen ch%u role=%s key=%d",
|
||||||
|
(unsigned)channel, App_Auto_RoleName(channel), key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
App_Auto_LogPitch(channel, key, out, flags, 1);
|
||||||
|
return out;
|
||||||
|
#else
|
||||||
|
(void)channel;
|
||||||
|
return key;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
static void Func_CallBack_ProgramChange(int channel, int program)
|
static void Func_CallBack_ProgramChange(int channel, int program)
|
||||||
{
|
{
|
||||||
MidiFifoItem_t midimsg;
|
MidiFifoItem_t midimsg;
|
||||||
midimsg.msg[0] = 0xc0 | channel;
|
uint8_t ch = (uint8_t)(channel & 0x0F);
|
||||||
midimsg.msg[1] = program;
|
LOG_I("REG", "pc ch%u prog=%d", (unsigned)channel, program);
|
||||||
|
/* 双音色库:伴奏一律先切 Bank0(CC0 MSB=0, CC32 LSB=0)再 PC */
|
||||||
|
midimsg.msg[0] = 0xb0 | ch;
|
||||||
|
midimsg.msg[1] = 0x00; /* CC0 Bank MSB */
|
||||||
|
midimsg.msg[2] = MIDI_BANK_ACCOMP;
|
||||||
|
midimsg.msglen = 3;
|
||||||
|
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
||||||
|
midimsg.msg[1] = 0x20; /* CC32 Bank LSB */
|
||||||
|
midimsg.msg[2] = 0;
|
||||||
|
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
||||||
|
midimsg.msg[0] = 0xc0 | ch;
|
||||||
|
midimsg.msg[1] = (uint8_t)program;
|
||||||
midimsg.msg[2] = 0;
|
midimsg.msg[2] = 0;
|
||||||
midimsg.msglen = 2;
|
midimsg.msglen = 2;
|
||||||
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
||||||
|
|
@ -32,8 +350,10 @@ static void Func_CallBack_ProgramChange(int channel, int program)
|
||||||
static void Func_CallBack_NoteOff(int channel,int key)
|
static void Func_CallBack_NoteOff(int channel,int key)
|
||||||
{
|
{
|
||||||
MidiFifoItem_t midimsg;
|
MidiFifoItem_t midimsg;
|
||||||
midimsg.msg[0] = 0x80 | channel;
|
uint8_t flags = 0;
|
||||||
midimsg.msg[1] = key;
|
int out_key = App_Auto_MapNote(channel, key, &flags);
|
||||||
|
midimsg.msg[0] = 0x80 | (channel & 0x0F);
|
||||||
|
midimsg.msg[1] = (uint8_t)out_key;
|
||||||
midimsg.msg[2] = 0;
|
midimsg.msg[2] = 0;
|
||||||
midimsg.msglen = 3;
|
midimsg.msglen = 3;
|
||||||
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
||||||
|
|
@ -43,9 +363,19 @@ static void Func_CallBack_NoteOff(int channel,int key)
|
||||||
static void Func_CallBack_NoteOn(int channel,int key,int vel)
|
static void Func_CallBack_NoteOn(int channel,int key,int vel)
|
||||||
{
|
{
|
||||||
MidiFifoItem_t midimsg;
|
MidiFifoItem_t midimsg;
|
||||||
midimsg.msg[0] = 0x90 | channel;
|
int out_key;
|
||||||
midimsg.msg[1] = key;
|
|
||||||
midimsg.msg[2] = vel;
|
if (vel == 0)
|
||||||
|
{
|
||||||
|
uint8_t flags = 0;
|
||||||
|
out_key = App_Auto_MapNote(channel, key, &flags); /* 视同 NoteOff */
|
||||||
|
}
|
||||||
|
else
|
||||||
|
out_key = App_Auto_MapNoteOn(channel, key);
|
||||||
|
|
||||||
|
midimsg.msg[0] = 0x90 | (channel & 0x0F);
|
||||||
|
midimsg.msg[1] = (uint8_t)out_key;
|
||||||
|
midimsg.msg[2] = (uint8_t)vel;
|
||||||
midimsg.msglen = 3;
|
midimsg.msglen = 3;
|
||||||
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
||||||
|
|
||||||
|
|
@ -96,6 +426,31 @@ static const char *ToneFlashMapBank(uint32_t abs)
|
||||||
|
|
||||||
static uint32_t s_flash_rd_last_base = 0xFFFFFFFFu;
|
static uint32_t s_flash_rd_last_base = 0xFFFFFFFFu;
|
||||||
static uint16_t s_flash_rd_log_cnt = 0;
|
static uint16_t s_flash_rd_log_cnt = 0;
|
||||||
|
/* 最近一次 LoadPreset 是否成功;失败时禁止拨片起奏(防空库 + TMR6 ISR 崩) */
|
||||||
|
static volatile uint8_t s_preset_ready = 0;
|
||||||
|
|
||||||
|
int App_Auto_LoadPresetFromFlash(int presetIndex)
|
||||||
|
{
|
||||||
|
int ret = AutoBandTop1_LoadPresetItemFromFlash(presetIndex);
|
||||||
|
if (ret == 0)
|
||||||
|
{
|
||||||
|
const char *name = AutoBandTop1_GetPresetName();
|
||||||
|
int count = AutoBandTop1_GetPresetItemCount();
|
||||||
|
s_preset_ready = (count > 0 && name != NULL && name[0] != '\0') ? 1u : 0u;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
s_preset_ready = 0u;
|
||||||
|
AutoBandTop1_Stop();
|
||||||
|
StartFlag = 0;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t App_Auto_IsPresetReady(void)
|
||||||
|
{
|
||||||
|
return s_preset_ready;
|
||||||
|
}
|
||||||
|
|
||||||
static int Func_CallBack_ReadFlash(int address,int length,uint8_t* pOutput)
|
static int Func_CallBack_ReadFlash(int address,int length,uint8_t* pOutput)
|
||||||
{
|
{
|
||||||
|
|
@ -135,6 +490,9 @@ static int Func_CallBack_ReadFlash(int address,int length,uint8_t* pOutput)
|
||||||
//
|
//
|
||||||
void App_Auto_Init(void)
|
void App_Auto_Init(void)
|
||||||
{
|
{
|
||||||
|
LOG_I("REG", "ch map bass=%d drum=%d fix=%d E1=%d E2=%d Gs4=%d",
|
||||||
|
BASS_CH, DRUM_CH, CHORD_REG_FIX_EN, MIDI_E1, MIDI_E2, MIDI_GSHARP4);
|
||||||
|
LOG_I("REG", "PITCH log: chN(role) in->out d=delta deg=I..VII xp=xpose xf=#Fextra fl=flags");
|
||||||
AutoBandTop1_SetPresetStoreBuffer(pStoreBuffer,PRESET_BUFFER_SIZE);
|
AutoBandTop1_SetPresetStoreBuffer(pStoreBuffer,PRESET_BUFFER_SIZE);
|
||||||
AutoBandTop1_Init();
|
AutoBandTop1_Init();
|
||||||
AutoBandTop1_RegisterCallBack_NoteOn(Func_CallBack_NoteOn);
|
AutoBandTop1_RegisterCallBack_NoteOn(Func_CallBack_NoteOn);
|
||||||
|
|
@ -145,11 +503,15 @@ void App_Auto_Init(void)
|
||||||
AutoBandTop1_RegisterReadFlashCallback(Func_CallBack_ReadFlash);
|
AutoBandTop1_RegisterReadFlashCallback(Func_CallBack_ReadFlash);
|
||||||
m_presetCount = AutoBandTop1_GetPresetItemCount();
|
m_presetCount = AutoBandTop1_GetPresetItemCount();
|
||||||
|
|
||||||
int ret = AutoBandTop1_LoadPresetItemFromFlash(0);
|
int ret = App_Auto_LoadPresetFromFlash(0);
|
||||||
if (ret != 0)
|
if (ret != 0)
|
||||||
{
|
{
|
||||||
LOG_E("AUTO", "LoadPreset fail ret=%d addr=0x%08X map=%s",
|
uint8_t hdr[16];
|
||||||
ret, (unsigned)ADDRESS, ToneFlashMapBank((uint32_t)ADDRESS));
|
W25Q128_Read(hdr, (uint32_t)ADDRESS, 16);
|
||||||
|
LOG_E("AUTO", "LoadPreset fail ret=%d addr=0x%08X map=%s magic=%02X%02X%02X%02X cnt=%lu",
|
||||||
|
ret, (unsigned)ADDRESS, ToneFlashMapBank((uint32_t)ADDRESS),
|
||||||
|
hdr[0], hdr[1], hdr[2], hdr[3],
|
||||||
|
(unsigned long)(hdr[12] | (hdr[13] << 8) | (hdr[14] << 16) | (hdr[15] << 24)));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,14 @@
|
||||||
#ifndef APP_AUTO_H_
|
#ifndef APP_AUTO_H_
|
||||||
#define APP_AUTO_H_
|
#define APP_AUTO_H_
|
||||||
#include "mymidififo1.h"
|
#include "mymidififo1.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
extern void App_Auto_Init(void);
|
extern void App_Auto_Init(void);
|
||||||
|
|
||||||
|
/* 统一加载入口:记录成功/失败,失败时 Stop,避免空库起奏进 ISR 崩溃 */
|
||||||
|
int App_Auto_LoadPresetFromFlash(int presetIndex);
|
||||||
|
uint8_t App_Auto_IsPresetReady(void);
|
||||||
|
|
||||||
extern MidiFifoSeq_t m_MidiSendFifo;
|
extern MidiFifoSeq_t m_MidiSendFifo;
|
||||||
|
|
||||||
#endif /* APP_AUTO_H_ */
|
#endif /* APP_AUTO_H_ */
|
||||||
|
|
|
||||||
45
UI/UI_Idle.c
45
UI/UI_Idle.c
|
|
@ -43,12 +43,6 @@ static void UI_ModeSelect_DrawButtons(void)
|
||||||
LCD_WR_PIC_FROM_FLASH(UI0902_MODE_ROW_X, y,
|
LCD_WR_PIC_FROM_FLASH(UI0902_MODE_ROW_X, y,
|
||||||
UI0902_MODE_ROW_W, UI0902_MODE_ROW_H,
|
UI0902_MODE_ROW_W, UI0902_MODE_ROW_H,
|
||||||
s_mode_row_addr[i]);
|
s_mode_row_addr[i]);
|
||||||
/* 万能模式素材顶部有残留近白横线(相对行内 y=7/8),盖成行底色 */
|
|
||||||
if (i == 0) {
|
|
||||||
LCD_FillByColor(UI0902_MODE_ROW_X, (uint16_t)(y + 7),
|
|
||||||
(uint16_t)(UI0902_MODE_ROW_X + UI0902_MODE_ROW_W),
|
|
||||||
(uint16_t)(y + 9), UI0902_ROW_BG);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,7 +85,14 @@ extern int8_t delta;
|
||||||
extern STRING_MIDI USE_MIDI;
|
extern STRING_MIDI USE_MIDI;
|
||||||
void LoadConfig(void)
|
void LoadConfig(void)
|
||||||
{
|
{
|
||||||
SendProgramChangeMidiEvent(0,Timbre_index[mGuiData[GUI_TIMBRE_SELECT].Current]);
|
uint8_t ch;
|
||||||
|
|
||||||
|
/* 双音色库:主弦走 Bank1,先选 bank 再 PC */
|
||||||
|
SendProgramWithBankMidiEvent(0, MIDI_BANK_STRING, Timbre_index[mGuiData[GUI_TIMBRE_SELECT].Current]);
|
||||||
|
|
||||||
|
/* 伴奏通道预置 Bank0,避免上电后首个 PC 前误用残留 bank */
|
||||||
|
for (ch = 1; ch <= 15; ch++)
|
||||||
|
SendBankChangeMidiEvent(ch, MIDI_BANK_ACCOMP);
|
||||||
|
|
||||||
uint8_t val_127 = (faders[0].val * 127) / 10;
|
uint8_t val_127 = (faders[0].val * 127) / 10;
|
||||||
SendVolumeChangeMidiEvent(1,val_127);
|
SendVolumeChangeMidiEvent(1,val_127);
|
||||||
|
|
@ -147,40 +148,44 @@ void Touch_Mode_Select_Action(int8_t FLAG, uint8_t ID, uint8_t areaIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
extern uint8_t bat_init_done;
|
extern uint8_t bat_init_done;
|
||||||
|
/* 关机充电页已绘制标志:POWER_ON 需清除,以便再次软关机充电时重绘 */
|
||||||
|
static bool s_charge_ui_drawn = false;
|
||||||
void IdleProcess(DisplayTaskMessage_Type msg)
|
void IdleProcess(DisplayTaskMessage_Type msg)
|
||||||
{
|
{
|
||||||
uint8_t vol;
|
|
||||||
switch (msg.MessageType)
|
switch (msg.MessageType)
|
||||||
{
|
{
|
||||||
case MSG_ID_POWER_ON :
|
case MSG_ID_POWER_ON :
|
||||||
|
/* 充电软关机开机:滞后的拔电消息可能已切主电/关显示,强制恢复 */
|
||||||
|
BSP_MainPowerEnable(1);
|
||||||
|
LCD_WR_REG(0x29); /* Display ON */
|
||||||
|
s_charge_ui_drawn = false;
|
||||||
LCD_BLK_Set();
|
LCD_BLK_Set();
|
||||||
LCD_WR_PIC_FROM_FLASH(0, 0, UI0902_BOOT_LOGO_W, UI0902_BOOT_LOGO_H,
|
LCD_WR_PIC_FROM_FLASH(0, 0, UI0902_BOOT_LOGO_W, UI0902_BOOT_LOGO_H,
|
||||||
UI0902_BOOT_LOGO_ADDR);
|
UI0902_BOOT_LOGO_ADDR);
|
||||||
BSP_SelectAdcChannel(6);
|
|
||||||
/* 让出 CPU,避免忙等卡住触摸/主循环线程 */
|
/* 让出 CPU,避免忙等卡住触摸/主循环线程 */
|
||||||
rt_thread_mdelay(1000);
|
rt_thread_mdelay(1000);
|
||||||
LoadConfig();
|
LoadConfig();
|
||||||
vol = ADC_ReadChannel(ADC_CHANNEL_1)>>5;
|
/* 主音量不在此现场切通道读 ADC(会与扫描任务串通道):
|
||||||
app_adc_sync_volume(vol);
|
* 待 StartScanTask 后由 MasterVolume_Reapply 统一补发 */
|
||||||
Send_volume(vol);
|
|
||||||
|
|
||||||
UI_DrawModeSelectScreen();
|
UI_DrawModeSelectScreen();
|
||||||
TM1629D_AllLedOn(LED_COLOR_G);
|
TM1629D_AllLedOn(LED_COLOR_G);
|
||||||
TM1629D_UpdateDisplay(0);
|
TM1629D_UpdateDisplay(0);
|
||||||
StartTouchTask();
|
StartTouchTask();
|
||||||
StartScanTask();
|
StartScanTask();
|
||||||
/* Dream 上电稍后可能忽略首包 SysEx:再发主音量 + 调音台(保持关机前设置) */
|
/* Dream 上电稍后可能忽略首包 SysEx:再发主音量 + 调音台(保持关机前设置)。
|
||||||
|
* 此时扫描任务已启动、模拟开关在轮询,不可现场重读 ADC(会串通道),
|
||||||
|
* 统一用 MasterVolume_Reapply 取扫描任务维护的最近一次有效音量 */
|
||||||
rt_thread_mdelay(400);
|
rt_thread_mdelay(400);
|
||||||
vol = ADC_ReadChannel(ADC_CHANNEL_1)>>5;
|
MasterVolume_Reapply();
|
||||||
app_adc_sync_volume(vol);
|
|
||||||
Send_volume(vol);
|
|
||||||
LoadConfig();
|
LoadConfig();
|
||||||
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, level, 1, WHITE, GRAY);
|
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, level, 1, WHITE, GRAY);
|
||||||
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 +195,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;
|
||||||
|
|
@ -230,7 +237,9 @@ void IdleProcess(DisplayTaskMessage_Type msg)
|
||||||
|
|
||||||
case MSG_ID_POWOFF_CHARG:
|
case MSG_ID_POWOFF_CHARG:
|
||||||
{
|
{
|
||||||
static bool s_charge_ui_drawn = false;
|
/* 已开机:忽略滞后的关机充电消息,勿覆盖模式选择页或切断主电 */
|
||||||
|
if (powon)
|
||||||
|
break;
|
||||||
if(!s_charge_ui_drawn)
|
if(!s_charge_ui_drawn)
|
||||||
{
|
{
|
||||||
uint8_t peek[4];
|
uint8_t peek[4];
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,33 @@ void DrawAllFaders(void)
|
||||||
drv_nvm_save_to_flash();
|
drv_nvm_save_to_flash();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 0902-06:实心蓝色圆钮(勿用同心空心圆,会透出色条) */
|
/* 0902-06:圆钮定位半径 = 色条半宽;填充略大以盖住柱端抗锯齿白边(夹紧不画出柱外) */
|
||||||
#define MIXER_KNOB_R 14
|
#define MIXER_KNOB_R 15
|
||||||
|
#define MIXER_KNOB_FILL_R 16
|
||||||
|
#define MIXER_TRACK_HALF_W 15
|
||||||
|
|
||||||
|
/* 与柱端半圆同形的实心圆钮:顶/底档位与色条圆角完美贴合,无虚线白边 */
|
||||||
|
static void DrawMixerKnob(uint16_t cx, uint16_t cy)
|
||||||
|
{
|
||||||
|
int16_t dy;
|
||||||
|
int32_t r2 = (int32_t)MIXER_KNOB_FILL_R * (int32_t)MIXER_KNOB_FILL_R;
|
||||||
|
for (dy = -(int16_t)MIXER_KNOB_R; dy <= (int16_t)MIXER_KNOB_R; dy++)
|
||||||
|
{
|
||||||
|
int32_t xx = r2 - (int32_t)dy * (int32_t)dy;
|
||||||
|
int16_t dx = 0;
|
||||||
|
int16_t half;
|
||||||
|
if (xx < 0)
|
||||||
|
continue;
|
||||||
|
while ((int32_t)(dx + 1) * (dx + 1) <= xx)
|
||||||
|
dx++;
|
||||||
|
half = dx;
|
||||||
|
if (half > (int16_t)MIXER_TRACK_HALF_W)
|
||||||
|
half = (int16_t)MIXER_TRACK_HALF_W;
|
||||||
|
LCD_FillByColor((uint16_t)(cx - half), (uint16_t)(cy + dy),
|
||||||
|
(uint16_t)(cx + half + 1), (uint16_t)(cy + dy + 1),
|
||||||
|
COLOR_GRAD_MID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void DrawFaderSlider(Fader_t *f, uint16_t slider_y)
|
static void DrawFaderSlider(Fader_t *f, uint16_t slider_y)
|
||||||
{
|
{
|
||||||
|
|
@ -62,9 +87,9 @@ static void DrawFaderSlider(Fader_t *f, uint16_t slider_y)
|
||||||
uint16_t ly = f->y + f->h + 8;
|
uint16_t ly = f->y + f->h + 8;
|
||||||
uint16_t cy = (uint16_t)(slider_y + MIXER_KNOB_R);
|
uint16_t cy = (uint16_t)(slider_y + MIXER_KNOB_R);
|
||||||
char buf[8];
|
char buf[8];
|
||||||
LCD_FillCircle(cx, cy, MIXER_KNOB_R, COLOR_GRAD_MID);
|
DrawMixerKnob(cx, cy);
|
||||||
sprintf(buf, "%d", f->val);
|
sprintf(buf, "%d", f->val);
|
||||||
LCD_ShowString_AutoAlign((uint16_t)(cx - 8), (uint16_t)(cx + 8),
|
LCD_ShowString_AutoAlign((uint16_t)(cx - 10), (uint16_t)(cx + 10),
|
||||||
(uint16_t)(cy - 5),
|
(uint16_t)(cy - 5),
|
||||||
(uint8_t*)buf, LCD_ALIGN_CENTER, WHITE, COLOR_GRAD_MID, 0, 11);
|
(uint8_t*)buf, LCD_ALIGN_CENTER, WHITE, COLOR_GRAD_MID, 0, 11);
|
||||||
LCD_FillByColor(cx - 15, ly + 17, cx + 15, ly + 19, UI0902_BG_COLOR);
|
LCD_FillByColor(cx - 15, ly + 17, cx + 15, ly + 19, UI0902_BG_COLOR);
|
||||||
|
|
@ -149,17 +174,14 @@ void UpdateFaderByTouch(int idx, uint16_t y)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ??????Y????????val? 0~10??
|
// 滑块顶边 Y:val0/10 时圆心落在色条底/顶半圆中心,两端对称贴合
|
||||||
// ============================================================
|
// ============================================================
|
||||||
static uint16_t CalcSliderY(Fader_t *f)
|
static uint16_t CalcSliderY(Fader_t *f)
|
||||||
{
|
{
|
||||||
uint16_t slider_h = (uint16_t)(MIXER_KNOB_R * 2);
|
uint16_t top_cy = (uint16_t)(f->y + MIXER_KNOB_R);
|
||||||
uint16_t fill_h = (f->val * f->h) / 10;
|
uint16_t bot_cy = (uint16_t)(f->y + f->h - 1 - MIXER_KNOB_R);
|
||||||
if (fill_h > f->h) fill_h = f->h;
|
uint16_t cy = (uint16_t)(bot_cy - ((uint32_t)(bot_cy - top_cy) * f->val) / 10);
|
||||||
uint16_t slider_y = f->y + f->h - fill_h - (slider_h / 2);
|
return (uint16_t)(cy - MIXER_KNOB_R);
|
||||||
if (slider_y < f->y) slider_y = f->y;
|
|
||||||
if (slider_y > f->y + f->h - slider_h) slider_y = f->y + f->h - slider_h;
|
|
||||||
return slider_y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int CheckTouchFader(uint16_t x, uint16_t y)
|
int CheckTouchFader(uint16_t x, uint16_t y)
|
||||||
|
|
@ -222,7 +244,7 @@ void UI_Mixer_Process(DisplayTaskMessage_Type msg)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MSG_ID_EC_VOL:
|
case MSG_ID_EC_VOL:
|
||||||
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, msg.HiByte, 1, WHITE, GRAY);
|
/* 调音台子页无状态栏音量条(与返回键重叠);主音量已在 MainTask 下发 */
|
||||||
ResetAutoPowerCount();
|
ResetAutoPowerCount();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -180,11 +180,11 @@ void Touch_Action(int8_t FLAG, uint8_t ID, uint8_t areaIndex,uint8_t value)
|
||||||
UI_ApplyToneAddress();
|
UI_ApplyToneAddress();
|
||||||
int ret = 0;
|
int ret = 0;
|
||||||
if (tab == 0)
|
if (tab == 0)
|
||||||
ret = AutoBandTop1_LoadPresetItemFromFlash(ParamGuiData[ALL_MODE_PARAM].Current);
|
ret = App_Auto_LoadPresetFromFlash(ParamGuiData[ALL_MODE_PARAM].Current);
|
||||||
else if (mGuiData[GUI_AUTOBAND_SW].Current)
|
else if (mGuiData[GUI_AUTOBAND_SW].Current)
|
||||||
ret = AutoBandTop1_LoadPresetItemFromFlash(ParamGuiData[EXPRESS_MODE_PARAM].Current);
|
ret = App_Auto_LoadPresetFromFlash(ParamGuiData[EXPRESS_MODE_PARAM].Current);
|
||||||
else
|
else
|
||||||
ret = AutoBandTop1_LoadPresetItemFromFlash(ParamGuiData[SONG_MODE_PARAM].Current);
|
ret = App_Auto_LoadPresetFromFlash(ParamGuiData[SONG_MODE_PARAM].Current);
|
||||||
if (ret != 0)
|
if (ret != 0)
|
||||||
LOG_E("tone", "load preset failed tab=%d sw=%d ret=%d addr=0x%08X",
|
LOG_E("tone", "load preset failed tab=%d sw=%d ret=%d addr=0x%08X",
|
||||||
tab, mGuiData[GUI_AUTOBAND_SW].Current, ret, (unsigned)ADDRESS);
|
tab, mGuiData[GUI_AUTOBAND_SW].Current, ret, (unsigned)ADDRESS);
|
||||||
|
|
@ -237,12 +237,24 @@ void Touch_Action(int8_t FLAG, uint8_t ID, uint8_t areaIndex,uint8_t value)
|
||||||
}
|
}
|
||||||
else if(ID==GUI_PLAY_SECTION)
|
else if(ID==GUI_PLAY_SECTION)
|
||||||
{
|
{
|
||||||
|
/* 万能模式第4键:改为尾奏播放(与独立尾奏键同路径),不走 main4 */
|
||||||
|
if (mGuiData[GUI_TAB_INDEX].Current == 0 && value == 3)
|
||||||
|
{
|
||||||
|
last_value = mGuiData[ID].Current;
|
||||||
|
mGuiData[ID].Current = value;
|
||||||
|
AutoBand_StartOutro();
|
||||||
|
if (!UI_SetFocus(GUI_PLAY_SECTION))
|
||||||
|
Refresh_SectionSelect(last_value, &UI_Label[ID], &mGuiData[ID]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(mGuiData[ID].Current != value)
|
if(mGuiData[ID].Current != value)
|
||||||
{
|
{
|
||||||
mGuiData[ID].Current = value;
|
mGuiData[ID].Current = value;
|
||||||
AutoBandTop1_SetRunVar(mGuiData[ID].Current);
|
AutoBandTop1_SetRunVar(mGuiData[ID].Current);
|
||||||
}else
|
}else
|
||||||
{
|
{
|
||||||
|
/* 触摸起奏前补发主音量,保证实际音量与图标一致 */
|
||||||
|
MasterVolume_Reapply();
|
||||||
AutoBandTop1_FillinWithIndex(value);
|
AutoBandTop1_FillinWithIndex(value);
|
||||||
AutoBandTop1_Start();
|
AutoBandTop1_Start();
|
||||||
}
|
}
|
||||||
|
|
@ -268,6 +280,8 @@ void Touch_Action(int8_t FLAG, uint8_t ID, uint8_t areaIndex,uint8_t value)
|
||||||
if(mGuiData[GUI_TAB_LAST_INDEX].Current == mGuiData[ID].Current)
|
if(mGuiData[GUI_TAB_LAST_INDEX].Current == mGuiData[ID].Current)
|
||||||
break;
|
break;
|
||||||
StartFlag = 0;
|
StartFlag = 0;
|
||||||
|
KEY_ID_1629 = 0; /* 清专业等残留指板键,拨片按当前调走走向 */
|
||||||
|
PressFlag = 0;
|
||||||
AutoBandTop1_Stop();
|
AutoBandTop1_Stop();
|
||||||
UI_ReloadTonePreset(); /* 切换模式必须换库,避免专业仍播万能数据 */
|
UI_ReloadTonePreset(); /* 切换模式必须换库,避免专业仍播万能数据 */
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ const Touch_AreaTypeDef Touch_SetingSelect_Areas[] = {
|
||||||
{ UI0902_HUB_CARD_X, (uint16_t)(UI0902_HUB_CARD_X + UI0902_HUB_CARD_W - 1),
|
{ UI0902_HUB_CARD_X, (uint16_t)(UI0902_HUB_CARD_X + UI0902_HUB_CARD_W - 1),
|
||||||
UI0902_HUB_CARD_Y1, (uint16_t)(UI0902_HUB_CARD_Y1 + UI0902_HUB_CARD_H - 1),
|
UI0902_HUB_CARD_Y1, (uint16_t)(UI0902_HUB_CARD_Y1 + UI0902_HUB_CARD_H - 1),
|
||||||
0, 0, GUI_SETING_SELECT3, NULL },
|
0, 0, GUI_SETING_SELECT3, NULL },
|
||||||
/* 底栏四键 */
|
/* 底栏四键:与 Touch_Areas 共用 UI0902_NAV_* 几何 */
|
||||||
{ 0, 69, 280, 319, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(0), UI0902_NAV_X1(0), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
|
||||||
{ 70, 129, 280, 319, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(1), UI0902_NAV_X1(1), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
|
||||||
{ 130, 184, 280, 319, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(2), UI0902_NAV_X1(2), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
|
||||||
{ 185, 239, 280, 319, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(3), UI0902_NAV_X1(3), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
|
||||||
};
|
};
|
||||||
|
|
||||||
#define TOUCH_AREA_SETING_SELECT_NUM (sizeof(Touch_SetingSelect_Areas)/sizeof(Touch_AreaTypeDef))
|
#define TOUCH_AREA_SETING_SELECT_NUM (sizeof(Touch_SetingSelect_Areas)/sizeof(Touch_AreaTypeDef))
|
||||||
|
|
@ -124,6 +124,11 @@ void UI_SystemSet_Process(DisplayTaskMessage_Type msg)
|
||||||
label_top();
|
label_top();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case MSG_ID_EC_VOL:
|
||||||
|
/* 主音量已在 MainTask 下发;此处只刷新状态栏音量条,保证与旋钮一致 */
|
||||||
|
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, msg.HiByte, 1, WHITE, GRAY);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
204
UI/UI_global.c
204
UI/UI_global.c
|
|
@ -54,6 +54,7 @@ void CallUI_Idle(void) {
|
||||||
|
|
||||||
void CallUI_SongMode(void) {
|
void CallUI_SongMode(void) {
|
||||||
app_touch_suppress(500);
|
app_touch_suppress(500);
|
||||||
|
MasterVolume_Reapply(); /* 进模式补发主音量,防开机 SysEx 丢失后实际音量与图标不符 */
|
||||||
UI_SongMode_Init();
|
UI_SongMode_Init();
|
||||||
CurrUIProcress = UI_SongMode_Process;
|
CurrUIProcress = UI_SongMode_Process;
|
||||||
app_log_set_ui_page("Song");
|
app_log_set_ui_page("Song");
|
||||||
|
|
@ -62,6 +63,7 @@ void CallUI_SongMode(void) {
|
||||||
|
|
||||||
void CallUI_ExpertMode(void) {
|
void CallUI_ExpertMode(void) {
|
||||||
app_touch_suppress(500);
|
app_touch_suppress(500);
|
||||||
|
MasterVolume_Reapply();
|
||||||
UI_ExpertMode_Init();
|
UI_ExpertMode_Init();
|
||||||
// CurrUIProcress = UI_ExpertMode_Process;
|
// CurrUIProcress = UI_ExpertMode_Process;
|
||||||
CurrUIProcress = UI_SongMode_Process;
|
CurrUIProcress = UI_SongMode_Process;
|
||||||
|
|
@ -71,6 +73,7 @@ void CallUI_ExpertMode(void) {
|
||||||
|
|
||||||
void CallUI_FreeMode(void) {
|
void CallUI_FreeMode(void) {
|
||||||
app_touch_suppress(500);
|
app_touch_suppress(500);
|
||||||
|
MasterVolume_Reapply();
|
||||||
UI_FreeMode_Init();
|
UI_FreeMode_Init();
|
||||||
// CurrUIProcress = UI_FreeMode_Process;
|
// CurrUIProcress = UI_FreeMode_Process;
|
||||||
CurrUIProcress = UI_SongMode_Process;
|
CurrUIProcress = UI_SongMode_Process;
|
||||||
|
|
@ -167,11 +170,24 @@ void UI_ReloadTonePreset(void)
|
||||||
/* 万能:和弦走向 */
|
/* 万能:和弦走向 */
|
||||||
if (mGuiData[GUI_TAB_INDEX].Current == 0)
|
if (mGuiData[GUI_TAB_INDEX].Current == 0)
|
||||||
{
|
{
|
||||||
int ret = AutoBandTop1_LoadPresetItemFromFlash(ParamGuiData[ALL_MODE_PARAM].Current);
|
int ret = App_Auto_LoadPresetFromFlash(ParamGuiData[ALL_MODE_PARAM].Current);
|
||||||
LOG_I("tone", "load universal idx=%d ret=%d name=%s addr=0x%08X map=BIN3@0x%08X",
|
if (ret != 0)
|
||||||
ParamGuiData[ALL_MODE_PARAM].Current, ret,
|
{
|
||||||
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
uint8_t hdr[16];
|
||||||
(unsigned)ADDRESS, (unsigned)EXTFLASH_BIN3_UNIVERSAL_ADDR);
|
W25Q128_Read(hdr, (uint32_t)ADDRESS, 16);
|
||||||
|
LOG_E("tone", "load universal idx=%d ret=%d magic=%02X%02X%02X%02X cnt=%lu addr=0x%08X",
|
||||||
|
ParamGuiData[ALL_MODE_PARAM].Current, ret,
|
||||||
|
hdr[0], hdr[1], hdr[2], hdr[3],
|
||||||
|
(unsigned long)(hdr[12] | (hdr[13] << 8) | (hdr[14] << 16) | (hdr[15] << 24)),
|
||||||
|
(unsigned)ADDRESS);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG_I("tone", "load universal idx=%d ret=%d name=%s addr=0x%08X map=BIN3@0x%08X",
|
||||||
|
ParamGuiData[ALL_MODE_PARAM].Current, ret,
|
||||||
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
||||||
|
(unsigned)ADDRESS, (unsigned)EXTFLASH_BIN3_UNIVERSAL_ADDR);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/* 普通/专业:本地曲目或节奏类型 */
|
/* 普通/专业:本地曲目或节奏类型 */
|
||||||
|
|
@ -181,7 +197,7 @@ void UI_ReloadTonePreset(void)
|
||||||
local->Max = (LOCAL_SONG_COUNT > 0) ? (uint16_t)(LOCAL_SONG_COUNT - 1) : 0;
|
local->Max = (LOCAL_SONG_COUNT > 0) ? (uint16_t)(LOCAL_SONG_COUNT - 1) : 0;
|
||||||
if (local->Current > local->Max)
|
if (local->Current > local->Max)
|
||||||
local->Current = local->Max;
|
local->Current = local->Max;
|
||||||
int ret = AutoBandTop1_LoadPresetItemFromFlash(local->Current);
|
int ret = App_Auto_LoadPresetFromFlash(local->Current);
|
||||||
if (ret != 0)
|
if (ret != 0)
|
||||||
LOG_E("tone", "load local song idx=%d failed ret=%d addr=0x%08X map=BIN2@0x%08X",
|
LOG_E("tone", "load local song idx=%d failed ret=%d addr=0x%08X map=BIN2@0x%08X",
|
||||||
local->Current, ret, (unsigned)ADDRESS,
|
local->Current, ret, (unsigned)ADDRESS,
|
||||||
|
|
@ -201,7 +217,7 @@ void UI_ReloadTonePreset(void)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
int ret = AutoBandTop1_LoadPresetItemFromFlash(ParamGuiData[SONG_MODE_PARAM].Current);
|
int ret = App_Auto_LoadPresetFromFlash(ParamGuiData[SONG_MODE_PARAM].Current);
|
||||||
LOG_I("tone", "load rhythm idx=%d ret=%d name=%s addr=0x%08X map=BIN1@0x%08X %s",
|
LOG_I("tone", "load rhythm idx=%d ret=%d name=%s addr=0x%08X map=BIN1@0x%08X %s",
|
||||||
ParamGuiData[SONG_MODE_PARAM].Current, ret,
|
ParamGuiData[SONG_MODE_PARAM].Current, ret,
|
||||||
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
||||||
|
|
@ -443,6 +459,58 @@ void Show_Battery_Icon(uint8_t flag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 中英文混排像素宽(与 LCD_GetMixedStringWidth 规则一致:CJK=sizey,ASCII=UI0902_ASCII_W) */
|
||||||
|
static uint16_t UI_MixedTextWidth(const uint8_t *str, uint8_t sizey)
|
||||||
|
{
|
||||||
|
uint16_t width = 0;
|
||||||
|
uint8_t ascii_w = UI0902_ASCII_W(sizey);
|
||||||
|
|
||||||
|
if (str == NULL)
|
||||||
|
return 0;
|
||||||
|
while (*str != '\0') {
|
||||||
|
if (*str > 0x80) {
|
||||||
|
width = (uint16_t)(width + sizey);
|
||||||
|
str += 2;
|
||||||
|
} else {
|
||||||
|
width = (uint16_t)(width + ascii_w);
|
||||||
|
str += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 曲目/音色共用:序号与名称间距(与「1.名称」拼接一致,保证两行视觉间距相同) */
|
||||||
|
#define UI0902_INDEX_NAME_GAP 0
|
||||||
|
|
||||||
|
/* 在 [x1,x2] 内整体居中绘制「N.」+ 名称(无渐变,show_Param 用) */
|
||||||
|
static void UI_DrawIndexedName_Mixed(uint16_t x1, uint16_t x2, uint16_t text_y,
|
||||||
|
const char *num, const char *name,
|
||||||
|
uint16_t fc, uint16_t bc, uint8_t sizey)
|
||||||
|
{
|
||||||
|
char line[48] = {0};
|
||||||
|
snprintf(line, sizeof(line), "%s%s", num, name);
|
||||||
|
LCD_ShowMixedString(x1, x2, text_y, (uint8_t *)line, LCD_ALIGN_CENTER,
|
||||||
|
fc, bc, sizey, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 在 [x1,x2] 内整体居中绘制「N.」+ 中文名(渐变底,音色行用);间隙与曲目行一致 */
|
||||||
|
static void UI_DrawIndexedName_Grad(uint16_t x1, uint16_t x2, uint16_t text_y,
|
||||||
|
const char *num, const uint8_t *name,
|
||||||
|
uint16_t fc, uint16_t c_top, uint16_t c_bot, uint8_t sizey)
|
||||||
|
{
|
||||||
|
uint16_t num_w = (uint16_t)(strlen(num) * UI0902_ASCII_W(sizey));
|
||||||
|
uint16_t name_w = UI_MixedTextWidth(name, sizey);
|
||||||
|
uint16_t total_w = (uint16_t)(num_w + UI0902_INDEX_NAME_GAP + name_w);
|
||||||
|
uint16_t area_w = (x2 > x1) ? (uint16_t)(x2 - x1) : 0;
|
||||||
|
uint16_t start_x = (total_w >= area_w) ? x1 : (uint16_t)(x1 + (area_w - total_w) / 2);
|
||||||
|
uint16_t name_x = (uint16_t)(start_x + num_w + UI0902_INDEX_NAME_GAP);
|
||||||
|
|
||||||
|
LCD_ShowString_AutoAlign_Gradient(start_x, (uint16_t)(start_x + num_w), text_y,
|
||||||
|
(const uint8_t *)num, LCD_ALIGN_LEFT, fc, c_top, c_bot, sizey);
|
||||||
|
LCD_ShowChinese_AutoAlign_Gradient(name_x, x2, text_y, name, LCD_ALIGN_LEFT,
|
||||||
|
fc, c_top, c_bot, sizey);
|
||||||
|
}
|
||||||
|
|
||||||
void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
||||||
{
|
{
|
||||||
char Textstr_new[40] = {0};
|
char Textstr_new[40] = {0};
|
||||||
|
|
@ -453,9 +521,19 @@ void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
||||||
const uint16_t text_y = row_y + (row_h - UI->Text_size) / 2 + 1;
|
const uint16_t text_y = row_y + (row_h - UI->Text_size) / 2 + 1;
|
||||||
uint8_t idx = Group->Current + 1;
|
uint8_t idx = Group->Current + 1;
|
||||||
uint16_t c_top, c_bot;
|
uint16_t c_top, c_bot;
|
||||||
|
uint16_t x1, x2;
|
||||||
|
|
||||||
UI_GetFocusGrad(GUI_MODE_PARAM, &c_top, &c_bot);
|
UI_GetFocusGrad(GUI_MODE_PARAM, &c_top, &c_bot);
|
||||||
|
|
||||||
|
x1 = (uint16_t)(UI->x + 10);
|
||||||
|
x2 = (uint16_t)(UI->x + UI->w - 10);
|
||||||
|
|
||||||
|
/* 修复曲目名尾部残留:旧名比新名长时,只填渐变胶囊(x+25 ~ x+w-40)
|
||||||
|
盖不住文字区(x+10 ~ x+w-10)内的旧笔画,切到短名后尾部留残影。
|
||||||
|
先按行底色清整个文字区,再补渐变与文字;整行清除会盖住两侧三角,
|
||||||
|
文字绘制后统一补画。 */
|
||||||
|
LCD_FillByColor(x1, row_y, x2, (uint16_t)(row_y + row_h), c_top);
|
||||||
|
|
||||||
LCD_FillRoundRectGradient(
|
LCD_FillRoundRectGradient(
|
||||||
UI->x+25, row_y,
|
UI->x+25, row_y,
|
||||||
UI->x + UI->w-40-(UI->x+25),
|
UI->x + UI->w-40-(UI->x+25),
|
||||||
|
|
@ -464,18 +542,21 @@ void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
||||||
c_top, c_bot
|
c_top, c_bot
|
||||||
);
|
);
|
||||||
|
|
||||||
// 拼接序号 1. 2. 3.
|
/* 序号+名称整体居中(约等于标注「右移7px」的视觉效果) */
|
||||||
sprintf(NumBuf, "%d.", idx);
|
sprintf(NumBuf, "%d.", idx);
|
||||||
uint16_t num_w = (uint16_t)(strlen(NumBuf) * UI0902_ASCII_W(UI->Text_size));
|
|
||||||
if (Group->Id == EXPRESS_MODE_PARAM && Group->Current < LOCAL_SONG_COUNT)
|
if (Group->Id == EXPRESS_MODE_PARAM && Group->Current < LOCAL_SONG_COUNT)
|
||||||
strcpy(Textstr_new, LocalSongNameGbk[Group->Current]);
|
strcpy(Textstr_new, LocalSongNameGbk[Group->Current]);
|
||||||
else
|
else
|
||||||
GET_ParamNum_Str(Group->Current, 0, Textstr_new, Group->Id);
|
GET_ParamNum_Str(Group->Current, 0, Textstr_new, Group->Id);
|
||||||
uint32_t string_start_x = LCD_ShowMixedString(UI->x+10, UI->x + UI->w, text_y, (uint8_t *)Textstr_new , LCD_ALIGN_CENTER, WHITE, BLACK, UI->Text_size, 1);
|
UI_DrawIndexedName_Mixed(x1, x2, text_y, NumBuf, Textstr_new,
|
||||||
//序号X = 中文起点 - 数字宽度 - 5,间<EFBC8C>?像素
|
WHITE, BLACK, UI->Text_size);
|
||||||
uint16_t num_x = string_start_x - num_w;
|
|
||||||
// 绘制左侧数字序号
|
/* 补画左右三角(几何与 label_ModeSelece_ui 一致) */
|
||||||
LCD_ShowString(num_x, text_y, (const uint8_t *)NumBuf, UI->Text_fc, UI->Text_bc, UI->Text_size, 1);
|
{
|
||||||
|
uint16_t row_cy = (uint16_t)(row_y + row_h / 2);
|
||||||
|
UI_DrawTriLeft((uint16_t)(UI->x + 5), row_cy);
|
||||||
|
UI_DrawTriRight((uint16_t)(UI->x + UI->w - 5 - UI0902_TRI_R_W), row_cy);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -803,12 +884,8 @@ void label_ModeSelece_ui(const LEBEL_UI *UI,GUI_SWITCH * Group)
|
||||||
paramCurData = &ParamGuiData[SONG_MODE_PARAM];
|
paramCurData = &ParamGuiData[SONG_MODE_PARAM];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 左右三角由 show_Param 末尾统一补画(整行清除后必须重画) */
|
||||||
show_Param(&UI_Label[GUI_MODE_PARAM], paramCurData);
|
show_Param(&UI_Label[GUI_MODE_PARAM], paramCurData);
|
||||||
{
|
|
||||||
uint16_t row_cy = UI->y + UI0902_TAB_H + 4 + (UI->h - UI0902_TAB_H - 4) / 2;
|
|
||||||
UI_DrawTriLeft(UI->x + 5, row_cy);
|
|
||||||
UI_DrawTriRight((uint16_t)(UI->x + UI->w - 5 - UI0902_TRI_R_W), row_cy);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void label_TimbreSelece_ui(const LEBEL_UI *UI, GUI_SWITCH * Group)
|
void label_TimbreSelece_ui(const LEBEL_UI *UI, GUI_SWITCH * Group)
|
||||||
|
|
@ -851,26 +928,22 @@ void label_TimbreSelece_ui(const LEBEL_UI *UI, GUI_SWITCH * Group)
|
||||||
c_top, c_bot
|
c_top, c_bot
|
||||||
);
|
);
|
||||||
sprintf(NumBuf, "%d.", idx);
|
sprintf(NumBuf, "%d.", idx);
|
||||||
uint16_t num_w = (uint16_t)(strlen(NumBuf) * UI0902_ASCII_W(UI->Text_size));
|
|
||||||
GET_Num_Str(mGuiData[UI->Id].Current, 0, (char*)Textstr, UI->Id);
|
GET_Num_Str(mGuiData[UI->Id].Current, 0, (char*)Textstr, UI->Id);
|
||||||
uint32_t chinese_start_x = LCD_ShowChinese_AutoAlign_Gradient(
|
UI_DrawIndexedName_Grad(
|
||||||
UI->x + num_w,
|
(uint16_t)(UI->x + 10),
|
||||||
UI->x + UI->w,
|
(uint16_t)(UI->x + UI->w - 10),
|
||||||
text_y,
|
text_y,
|
||||||
(const uint8_t*)Textstr,
|
NumBuf,
|
||||||
UI->Text_Align,
|
Textstr,
|
||||||
UI->Text_fc,
|
UI->Text_fc,
|
||||||
c_top, c_bot,
|
c_top, c_bot,
|
||||||
UI->Text_size
|
UI->Text_size
|
||||||
);
|
);
|
||||||
uint16_t num_x = chinese_start_x - num_w - 5;
|
|
||||||
LCD_ShowString_AutoAlign_Gradient(num_x, num_x + num_w, text_y,
|
|
||||||
(const uint8_t *)NumBuf, LCD_ALIGN_LEFT, UI->Text_fc,
|
|
||||||
c_top, c_bot, UI->Text_size);
|
|
||||||
switch(Group->Id)
|
switch(Group->Id)
|
||||||
{
|
{
|
||||||
case GUI_TIMBRE_SELECT:
|
case GUI_TIMBRE_SELECT:
|
||||||
SendProgramChangeMidiEvent(0,Timbre_index[Group->Current]);
|
/* 双音色库:主弦走 Bank1,先选 bank 再 PC */
|
||||||
|
SendProgramWithBankMidiEvent(0, MIDI_BANK_STRING, Timbre_index[Group->Current]);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|
@ -907,37 +980,32 @@ void UI_DrawSubPageHeader(const uint8_t *title_gbk)
|
||||||
|
|
||||||
void Draw_Bottom_Bar(uint8_t sel, uint8_t first_back)
|
void Draw_Bottom_Bar(uint8_t sel, uint8_t first_back)
|
||||||
{
|
{
|
||||||
/* 需求稿四键:万能 | 普通 | 专业 | 设置;first_back 时设置位仍高亮(返回用触控逻辑) */
|
/* 四键合成图(图标+原稿文字);统一 32 高,文字底边对齐 */
|
||||||
const uint8_t *icons[4];
|
const uint8_t *icons[4];
|
||||||
uint8_t ws[4], hs[4];
|
uint8_t ws[4];
|
||||||
/* 0904:四键中心约 40 / 100 / 160 / 210 */
|
|
||||||
const uint16_t cx[4] = { 40, 100, 160, 210 };
|
const uint16_t cx[4] = { 40, 100, 160, 210 };
|
||||||
|
const uint8_t icon_h = 32;
|
||||||
|
const uint16_t icon_y = 282;
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
|
|
||||||
(void)first_back;
|
(void)first_back;
|
||||||
/* 从段落钮底边之下清屏,勿切掉按钮底圆角(钮 y=231..264) */
|
|
||||||
LCD_FillByColor(0, (uint16_t)(UI0902_SECTION_Y + UI0902_SECTION_BTN_H),
|
LCD_FillByColor(0, (uint16_t)(UI0902_SECTION_Y + UI0902_SECTION_BTN_H),
|
||||||
240, 320, UI0902_BG_COLOR);
|
240, 320, UI0902_BG_COLOR);
|
||||||
/* mode.png:段落钮与底栏之间横线分隔(与顶栏同色同厚) */
|
|
||||||
LCD_FillByColor(0, UI0902_SEP_LINE_Y, 240,
|
LCD_FillByColor(0, UI0902_SEP_LINE_Y, 240,
|
||||||
(uint16_t)(UI0902_SEP_LINE_Y + UI0902_SEP_LINE_THICK),
|
(uint16_t)(UI0902_SEP_LINE_Y + UI0902_SEP_LINE_THICK),
|
||||||
UI0902_SEP_LINE_COLOR);
|
UI0902_SEP_LINE_COLOR);
|
||||||
|
|
||||||
if (sel == UI0902_NAV_UNIVERSAL) { icons[0] = gImage_Nav_Universal_Sel; ws[0] = 36; hs[0] = 28; }
|
icons[0] = (sel == UI0902_NAV_UNIVERSAL) ? gImage_Nav_Universal_Sel : gImage_Nav_Universal_Not;
|
||||||
else { icons[0] = gImage_Nav_Universal_Not; ws[0] = 36; hs[0] = 28; }
|
icons[1] = (sel == UI0902_NAV_NORMAL) ? gImage_Nav_Normal_Sel : gImage_Nav_Normal_Not;
|
||||||
|
icons[2] = (sel == UI0902_NAV_EXPERT) ? gImage_Nav_Expert_Sel : gImage_Nav_Expert_Not;
|
||||||
if (sel == UI0902_NAV_NORMAL) { icons[1] = gImage_Nav_Normal_Sel; ws[1] = 35; hs[1] = 29; }
|
icons[3] = (sel == UI0902_NAV_SETTING) ? gImage_Nav_Setting_Sel : gImage_Nav_Setting_Not;
|
||||||
else { icons[1] = gImage_Nav_Normal_Not; ws[1] = 35; hs[1] = 28; }
|
ws[0] = ws[1] = ws[2] = 40;
|
||||||
|
ws[3] = 22;
|
||||||
if (sel == UI0902_NAV_EXPERT) { icons[2] = gImage_Nav_Expert_Sel; ws[2] = 35; hs[2] = 27; }
|
|
||||||
else { icons[2] = gImage_Nav_Expert_Not; ws[2] = 35; hs[2] = 27; }
|
|
||||||
|
|
||||||
if (sel == UI0902_NAV_SETTING) { icons[3] = gImage_Nav_Setting_Sel; ws[3] = 18; hs[3] = 28; }
|
|
||||||
else { icons[3] = gImage_Nav_Setting_Not; ws[3] = 18; hs[3] = 28; }
|
|
||||||
|
|
||||||
for (i = 0; i < 4; i++)
|
for (i = 0; i < 4; i++)
|
||||||
{
|
{
|
||||||
LCD_WR_PIC_Trans(cx[i] - ws[i] / 2, 283, ws[i], hs[i], icons[i], BLACK);
|
LCD_WR_PIC_Trans((uint16_t)(cx[i] - ws[i] / 2), icon_y,
|
||||||
|
ws[i], icon_h, icons[i], BLACK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -988,7 +1056,7 @@ void Draw_Tab_Section_Menu(const LEBEL_UI *UI, TAB_SECTION_INDEX select_tab)
|
||||||
const uint8_t pitch = UI0902_SECTION_PITCH;
|
const uint8_t pitch = UI0902_SECTION_PITCH;
|
||||||
const uint16_t start_x = UI0902_MARGIN_X; /* 0902-04:12/69/126/183 */
|
const uint16_t start_x = UI0902_MARGIN_X; /* 0902-04:12/69/126/183 */
|
||||||
const uint16_t start_y = UI->y;
|
const uint16_t start_y = UI->y;
|
||||||
/* 0819:演奏段落数字与标题同为 13,按钮内垂直居中(视觉重心 +1) */
|
/* 0819:演奏段落数字与标题同为 13,按钮内垂直居中(视觉重心 +1);稿面数字整体右移 2px */
|
||||||
const uint16_t text_y = start_y + (tab_h - UI0902_FONT_BODY) / 2 + 1;
|
const uint16_t text_y = start_y + (tab_h - UI0902_FONT_BODY) / 2 + 1;
|
||||||
const char *nums[4] = {"1", "2", "3", "4"};
|
const char *nums[4] = {"1", "2", "3", "4"};
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
|
|
@ -999,7 +1067,7 @@ void Draw_Tab_Section_Menu(const LEBEL_UI *UI, TAB_SECTION_INDEX select_tab)
|
||||||
uint16_t bg = (select_tab == i) ? COLOR_GRAD_MID : UI0902_ROW_BG;
|
uint16_t bg = (select_tab == i) ? COLOR_GRAD_MID : UI0902_ROW_BG;
|
||||||
LCD_FillRoundRect(x, start_y, tab_w, tab_h, tab_r, bg);
|
LCD_FillRoundRect(x, start_y, tab_w, tab_h, tab_r, bg);
|
||||||
LCD_ShowString_AutoAlign(
|
LCD_ShowString_AutoAlign(
|
||||||
x, (uint16_t)(x + tab_w - 1), text_y,
|
(uint16_t)(x + 2), (uint16_t)(x + tab_w - 1 + 2), text_y,
|
||||||
(const uint8_t*)nums[i],
|
(const uint8_t*)nums[i],
|
||||||
UI->Text_Align,
|
UI->Text_Align,
|
||||||
WHITE,
|
WHITE,
|
||||||
|
|
@ -1223,11 +1291,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);
|
||||||
|
|
@ -1254,9 +1322,9 @@ void Draw_System_Item_List(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
||||||
WHITE, c_top, c_bot, value_sz);
|
WHITE, c_top, c_bot, value_sz);
|
||||||
}else
|
}else
|
||||||
{
|
{
|
||||||
/* 手册「不自动」;字库无「不」,界面用稿面「关机」 */
|
/* 手册「不自动」;字库无「不」,界面显示「关闭」(字库已补「闭」) */
|
||||||
LCD_ShowChinese_AutoAlign_Gradient(val_x1 + UI0902_TRI_L_W, val_x2 - UI0902_TRI_R_W, text_y,
|
LCD_ShowChinese_AutoAlign_Gradient(val_x1 + UI0902_TRI_L_W, val_x2 - UI0902_TRI_R_W, text_y,
|
||||||
(const uint8_t*)"\xB9\xD8\xBB\xFA",
|
(const uint8_t*)"\xB9\xD8\xB1\xD5",
|
||||||
LCD_ALIGN_CENTER, WHITE, c_top, c_bot, value_sz);
|
LCD_ALIGN_CENTER, WHITE, c_top, c_bot, value_sz);
|
||||||
}
|
}
|
||||||
UI_DrawTriRight((uint16_t)(val_x2 - UI0902_TRI_R_W), chev_cy);
|
UI_DrawTriRight((uint16_t)(val_x2 - UI0902_TRI_R_W), chev_cy);
|
||||||
|
|
@ -1264,13 +1332,17 @@ 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) */
|
/* 0902-07:右侧为进入下级页的细 chevron(设置图标-03),勿用实心三角 */
|
||||||
UI_DrawTriRight((uint16_t)(val_x2 - UI0902_TRI_R_W), chev_cy);
|
LCD_WR_PIC_FROM_FLASH_Trans(
|
||||||
|
(uint16_t)(val_x2 - UI0902_ICON_CHEVRON_W),
|
||||||
|
(uint16_t)(UI->y + (UI->h - UI0902_ICON_CHEVRON_H) / 2),
|
||||||
|
UI0902_ICON_CHEVRON_W, UI0902_ICON_CHEVRON_H,
|
||||||
|
UI0902_ICON_CHEVRON_ADDR);
|
||||||
break;
|
break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,19 +173,25 @@ void CallUI_RestoreSelect(void);
|
||||||
#define UI0902_NAV_EXPERT 2 /* 底栏:专业模式 */
|
#define UI0902_NAV_EXPERT 2 /* 底栏:专业模式 */
|
||||||
#define UI0902_NAV_SETTING 3 /* 底栏:设置 */
|
#define UI0902_NAV_SETTING 3 /* 底栏:设置 */
|
||||||
#define UI0902_NAV_NONE 0xFF
|
#define UI0902_NAV_NONE 0xFF
|
||||||
|
/* 底栏触区:段落钮底(y264)之下四等分全宽,各 60×55 */
|
||||||
|
#define UI0902_NAV_Y0 265
|
||||||
|
#define UI0902_NAV_Y1 319
|
||||||
|
#define UI0902_NAV_W 60
|
||||||
|
#define UI0902_NAV_X0(i) ((uint16_t)((i) * UI0902_NAV_W))
|
||||||
|
#define UI0902_NAV_X1(i) ((uint16_t)((i) * UI0902_NAV_W + UI0902_NAV_W - 1))
|
||||||
/* 兼容旧名(已废弃三键:设置|调音台|模式) */
|
/* 兼容旧名(已废弃三键:设置|调音台|模式) */
|
||||||
#define UI0902_NAV_MIXER UI0902_NAV_NORMAL
|
#define UI0902_NAV_MIXER UI0902_NAV_NORMAL
|
||||||
#define UI0902_NAV_MODE UI0902_NAV_EXPERT
|
#define UI0902_NAV_MODE UI0902_NAV_EXPERT
|
||||||
|
|
||||||
/* 底部图标实际内容(生成脚本曾把 万能/普通/专业/设置 误命名为 Setting/Mixer/Mode) */
|
/* 底部图标:0909 加大图标 + 原稿文字条(合成图);生成脚本曾误命名 Setting/Mixer/Mode */
|
||||||
#define gImage_Nav_Universal_Not gImage_UI0902_TabSetting_Sel_36x28 /* 底部-10 白 */
|
#define gImage_Nav_Universal_Not gImage_UI0902_TabSetting_Sel_40x32 /* 底部-10 白 */
|
||||||
#define gImage_Nav_Universal_Sel gImage_UI0902_TabMode_Sel_36x28 /* 底部-14 蓝 */
|
#define gImage_Nav_Universal_Sel gImage_UI0902_TabMode_Sel_40x32 /* 底部-14 蓝 */
|
||||||
#define gImage_Nav_Normal_Not gImage_UI0902_TabSetting_Not_35x28 /* 底部-11 白 */
|
#define gImage_Nav_Normal_Not gImage_UI0902_TabSetting_Not_40x32 /* 底部-11 白 */
|
||||||
#define gImage_Nav_Normal_Sel gImage_UI0902_TabMode_Not_35x29 /* 底部-15 蓝 */
|
#define gImage_Nav_Normal_Sel gImage_UI0902_TabMode_Not_40x32 /* 底部-15 蓝 */
|
||||||
#define gImage_Nav_Expert_Not gImage_UI0902_TabMixer_Sel_35x27 /* 底部-12 白 */
|
#define gImage_Nav_Expert_Not gImage_UI0902_TabMixer_Sel_40x32 /* 底部-12 白 */
|
||||||
#define gImage_Nav_Expert_Sel gImage_UI0902_TabBack_Sel_35x27 /* 底部-16 蓝 */
|
#define gImage_Nav_Expert_Sel gImage_UI0902_TabBack_Sel_40x32 /* 底部-16 蓝 */
|
||||||
#define gImage_Nav_Setting_Not gImage_UI0902_TabMixer_Not_18x28 /* 底部-13 白 */
|
#define gImage_Nav_Setting_Not gImage_UI0902_TabMixer_Not_22x32 /* 底部-13 白 */
|
||||||
#define gImage_Nav_Setting_Sel gImage_UI0902_TabBack_Not_18x28 /* 底部-17 蓝 */
|
#define gImage_Nav_Setting_Sel gImage_UI0902_TabBack_Not_22x32 /* 底部-17 蓝 */
|
||||||
/* 旧几何电池(Draw_Battery_Icon)最左起点;0902 图电池用 Draw_Status_Battery_Icon */
|
/* 旧几何电池(Draw_Battery_Icon)最左起点;0902 图电池用 Draw_Status_Battery_Icon */
|
||||||
#define UI_STATUS_BATTERY_X 150
|
#define UI_STATUS_BATTERY_X 150
|
||||||
#define UI_STATUS_BATTERY_H 11
|
#define UI_STATUS_BATTERY_H 11
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ const char CHS8Table[][2] = {
|
||||||
{0xC0,0xAB}, // 阔
|
{0xC0,0xAB}, // 阔
|
||||||
{0xCC,0xEC}, // 天
|
{0xCC,0xEC}, // 天
|
||||||
{0xBF,0xD5}, // 空
|
{0xBF,0xD5}, // 空
|
||||||
|
{0xB1,0xD5}, // 闭
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -125,6 +126,7 @@ const char CHS9Table[][2] = {
|
||||||
{0xC0,0xAB}, // 阔
|
{0xC0,0xAB}, // 阔
|
||||||
{0xCC,0xEC}, // 天
|
{0xCC,0xEC}, // 天
|
||||||
{0xBF,0xD5}, // 空
|
{0xBF,0xD5}, // 空
|
||||||
|
{0xB1,0xD5}, // 闭
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -191,6 +193,7 @@ const char CHS11Table[][2] = {
|
||||||
{0xC0,0xAB}, // 阔
|
{0xC0,0xAB}, // 阔
|
||||||
{0xCC,0xEC}, // 天
|
{0xCC,0xEC}, // 天
|
||||||
{0xBF,0xD5}, // 空
|
{0xBF,0xD5}, // 空
|
||||||
|
{0xB1,0xD5}, // 闭
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -256,6 +259,7 @@ const char CHS12Table[][2] = {
|
||||||
{0xC0,0xAB}, // 阔
|
{0xC0,0xAB}, // 阔
|
||||||
{0xCC,0xEC}, // 天
|
{0xCC,0xEC}, // 天
|
||||||
{0xBF,0xD5}, // 空
|
{0xBF,0xD5}, // 空
|
||||||
|
{0xB1,0xD5}, // 闭
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -449,6 +453,7 @@ const char CHS13Table[][2] = {
|
||||||
{0xC0,0xAB}, // 阔
|
{0xC0,0xAB}, // 阔
|
||||||
{0xCC,0xEC}, // 天
|
{0xCC,0xEC}, // 天
|
||||||
{0xBF,0xD5}, // 空
|
{0xBF,0xD5}, // 空
|
||||||
|
{0xB1,0xD5}, // 闭
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -641,6 +646,7 @@ const char CHS15Table[][2] = {
|
||||||
{0xC0,0xAB}, // 阔
|
{0xC0,0xAB}, // 阔
|
||||||
{0xCC,0xEC}, // 天
|
{0xCC,0xEC}, // 天
|
||||||
{0xBF,0xD5}, // 空
|
{0xBF,0xD5}, // 空
|
||||||
|
{0xB1,0xD5}, // 闭
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -837,6 +843,7 @@ const char CHS16Table[][2] = {
|
||||||
{0xC0,0xAB}, // 阔
|
{0xC0,0xAB}, // 阔
|
||||||
{0xCC,0xEC}, // 天
|
{0xCC,0xEC}, // 天
|
||||||
{0xBF,0xD5}, // 空
|
{0xBF,0xD5}, // 空
|
||||||
|
{0xB1,0xD5}, // 闭
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -871,6 +878,7 @@ const char CHS24Table[][2] = {
|
||||||
{0xB6, 0xA8},// 认
|
{0xB6, 0xA8},// 认
|
||||||
{0xB7, 0xD6},// 分
|
{0xB7, 0xD6},// 分
|
||||||
{0xD6, 0xD3},// 钟
|
{0xD6, 0xD3},// 钟
|
||||||
|
{0xB1,0xD5}, // 闭
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -930,6 +938,7 @@ const unsigned char Chinese_font_8[][8] = {
|
||||||
{0x1F,0x41,0x89,0x89,0xA9,0xDD,0xD7,0xC1},/*"阔",52*/
|
{0x1F,0x41,0x89,0x89,0xA9,0xDD,0xD7,0xC1},/*"阔",52*/
|
||||||
{0x7E,0x00,0x00,0xFF,0x18,0x00,0x00,0x81},/*"天",53*/
|
{0x7E,0x00,0x00,0xFF,0x18,0x00,0x00,0x81},/*"天",53*/
|
||||||
{0x18,0x41,0x00,0x00,0x3C,0x00,0x00,0x7F},/*"空",54*/
|
{0x18,0x41,0x00,0x00,0x3C,0x00,0x00,0x7F},/*"空",54*/
|
||||||
|
{0x4F,0x01,0x89,0xBD,0x99,0xA9,0x89,0x83},/*"闭",55*/
|
||||||
};
|
};
|
||||||
|
|
||||||
const unsigned char Chinese_font_12[][24] = {
|
const unsigned char Chinese_font_12[][24] = {
|
||||||
|
|
@ -993,6 +1002,7 @@ const unsigned char Chinese_font_12[][24] = {
|
||||||
{0x4F,0xE0,0x20,0x20,0xA1,0xA0,0x96,0x20,0xC2,0x20,0xAF,0xA0,0x92,0x20,0xAF,0xA0,0xE8,0xA0,0xAF,0xA0,0xA0,0x60,0x00,0x00},/*"阔",57*/
|
{0x4F,0xE0,0x20,0x20,0xA1,0xA0,0x96,0x20,0xC2,0x20,0xAF,0xA0,0x92,0x20,0xAF,0xA0,0xE8,0xA0,0xAF,0xA0,0xA0,0x60,0x00,0x00},/*"阔",57*/
|
||||||
{0x7F,0xC0,0x04,0x00,0x04,0x00,0x04,0x00,0xFF,0xE0,0x04,0x00,0x0A,0x00,0x0A,0x00,0x11,0x00,0x20,0x80,0xC0,0x60,0x00,0x00},/*"天",58*/
|
{0x7F,0xC0,0x04,0x00,0x04,0x00,0x04,0x00,0xFF,0xE0,0x04,0x00,0x0A,0x00,0x0A,0x00,0x11,0x00,0x20,0x80,0xC0,0x60,0x00,0x00},/*"天",58*/
|
||||||
{0x04,0x00,0x7F,0xE0,0x40,0x20,0x89,0x00,0x10,0x80,0x60,0x40,0x1F,0x80,0x04,0x00,0x04,0x00,0x04,0x00,0x7F,0xE0,0x00,0x00},/*"空",59*/
|
{0x04,0x00,0x7F,0xE0,0x40,0x20,0x89,0x00,0x10,0x80,0x60,0x40,0x1F,0x80,0x04,0x00,0x04,0x00,0x04,0x00,0x7F,0xE0,0x00,0x00},/*"空",59*/
|
||||||
|
{0x47,0xE0,0x20,0x20,0x82,0x20,0x82,0x20,0xBF,0xA0,0x86,0x20,0x8A,0x20,0x92,0x20,0xA2,0x20,0x86,0x20,0x80,0xE0,0x00,0x00},/*"闭",60*/
|
||||||
};
|
};
|
||||||
|
|
||||||
const unsigned char Chinese_font_9[][18] = {
|
const unsigned char Chinese_font_9[][18] = {
|
||||||
|
|
@ -1058,6 +1068,7 @@ const unsigned char Chinese_font_9[][18] = {
|
||||||
{0x5F,0x80,0x00,0x80,0x84,0x80,0x84,0x80,0x9E,0x80,0xA4,0x80,0xDE,0x80,0xDE,0x80,0x81,0x80},/*"阔",59*/
|
{0x5F,0x80,0x00,0x80,0x84,0x80,0x84,0x80,0x9E,0x80,0xA4,0x80,0xDE,0x80,0xDE,0x80,0x81,0x80},/*"阔",59*/
|
||||||
{0x7F,0x00,0x08,0x00,0x08,0x00,0xFF,0x80,0x08,0x00,0x14,0x00,0x14,0x00,0x00,0x00,0xC1,0x80},/*"天",60*/
|
{0x7F,0x00,0x08,0x00,0x08,0x00,0xFF,0x80,0x08,0x00,0x14,0x00,0x14,0x00,0x00,0x00,0xC1,0x80},/*"天",60*/
|
||||||
{0x08,0x00,0x7F,0x80,0x80,0x00,0x00,0x00,0x41,0x00,0x1E,0x00,0x08,0x00,0x08,0x00,0x7F,0x80},/*"空",61*/
|
{0x08,0x00,0x7F,0x80,0x80,0x00,0x00,0x00,0x41,0x00,0x1E,0x00,0x08,0x00,0x08,0x00,0x7F,0x80},/*"空",61*/
|
||||||
|
{0x4F,0x80,0x00,0x80,0x84,0x80,0xBE,0x80,0x8C,0x80,0x94,0x80,0xA4,0x80,0x8C,0x80,0x81,0x80},/*"闭",62*/
|
||||||
};
|
};
|
||||||
|
|
||||||
const unsigned char Chinese_font_11[][22] = {
|
const unsigned char Chinese_font_11[][22] = {
|
||||||
|
|
@ -1123,6 +1134,7 @@ const unsigned char Chinese_font_11[][22] = {
|
||||||
{0x4F,0xE0,0x20,0x20,0xA1,0xA0,0x96,0x20,0xC2,0x20,0xAF,0xA0,0x92,0x20,0xAF,0xA0,0xE8,0xA0,0xAF,0xA0,0xA0,0x60},/*"阔",59*/
|
{0x4F,0xE0,0x20,0x20,0xA1,0xA0,0x96,0x20,0xC2,0x20,0xAF,0xA0,0x92,0x20,0xAF,0xA0,0xE8,0xA0,0xAF,0xA0,0xA0,0x60},/*"阔",59*/
|
||||||
{0x7F,0xC0,0x04,0x00,0x04,0x00,0x04,0x00,0xFF,0xE0,0x04,0x00,0x0A,0x00,0x0A,0x00,0x11,0x00,0x20,0x80,0xC0,0x60},/*"天",60*/
|
{0x7F,0xC0,0x04,0x00,0x04,0x00,0x04,0x00,0xFF,0xE0,0x04,0x00,0x0A,0x00,0x0A,0x00,0x11,0x00,0x20,0x80,0xC0,0x60},/*"天",60*/
|
||||||
{0x04,0x00,0x7F,0xE0,0x40,0x20,0x89,0x00,0x10,0x80,0x60,0x40,0x1F,0x80,0x04,0x00,0x04,0x00,0x04,0x00,0x7F,0xE0},/*"空",61*/
|
{0x04,0x00,0x7F,0xE0,0x40,0x20,0x89,0x00,0x10,0x80,0x60,0x40,0x1F,0x80,0x04,0x00,0x04,0x00,0x04,0x00,0x7F,0xE0},/*"空",61*/
|
||||||
|
{0x47,0xE0,0x20,0x20,0x82,0x20,0x82,0x20,0xBF,0xA0,0x86,0x20,0x8A,0x20,0x92,0x20,0xA2,0x20,0x86,0x20,0x80,0xE0},/*"闭",62*/
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1320,6 +1332,7 @@ const unsigned char Chinese_font_13[][26] = {
|
||||||
{0x4F,0xE0,0x20,0x20,0xA1,0xA0,0x96,0x20,0xC2,0x20,0xAF,0xA0,0x92,0x20,0xAF,0xA0,0xE8,0xA0,0xAF,0xA0,0xA8,0xA0,0x80,0x60,0x00,0x00},/*"阔",186*/
|
{0x4F,0xE0,0x20,0x20,0xA1,0xA0,0x96,0x20,0xC2,0x20,0xAF,0xA0,0x92,0x20,0xAF,0xA0,0xE8,0xA0,0xAF,0xA0,0xA8,0xA0,0x80,0x60,0x00,0x00},/*"阔",186*/
|
||||||
{0x7F,0xE0,0x04,0x00,0x04,0x00,0x04,0x00,0xFF,0xF0,0x04,0x00,0x0A,0x00,0x0A,0x00,0x11,0x00,0x10,0x80,0x20,0x40,0xC0,0x30,0x00,0x00},/*"天",187*/
|
{0x7F,0xE0,0x04,0x00,0x04,0x00,0x04,0x00,0xFF,0xF0,0x04,0x00,0x0A,0x00,0x0A,0x00,0x11,0x00,0x10,0x80,0x20,0x40,0xC0,0x30,0x00,0x00},/*"天",187*/
|
||||||
{0x04,0x00,0xFF,0xE0,0x80,0x20,0x91,0x20,0x20,0x80,0xC0,0x40,0x3F,0x80,0x04,0x00,0x04,0x00,0x04,0x00,0x04,0x00,0xFF,0xE0,0x00,0x00},/*"空",188*/
|
{0x04,0x00,0xFF,0xE0,0x80,0x20,0x91,0x20,0x20,0x80,0xC0,0x40,0x3F,0x80,0x04,0x00,0x04,0x00,0x04,0x00,0x04,0x00,0xFF,0xE0,0x00,0x00},/*"空",188*/
|
||||||
|
{0x4F,0xE0,0x20,0x20,0x42,0x20,0x42,0x20,0x7F,0xA0,0x42,0x20,0x46,0x20,0x4A,0x20,0x52,0x20,0x62,0x20,0x46,0x20,0x40,0x60,0x00,0x00},/*"闭",189*/
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1514,6 +1527,7 @@ const unsigned char Chinese_font_15[][30] = {
|
||||||
{0x20,0x08,0x1B,0xFC,0x08,0x08,0x50,0x68,0x4B,0x88,0x40,0x88,0x57,0xF8,0x48,0x88,0x4B,0xE8,0x5A,0x28,0x4A,0x28,0x4B,0xE8,0x4A,0x28,0x40,0x18,0x00,0x00},/*"阔",184*/
|
{0x20,0x08,0x1B,0xFC,0x08,0x08,0x50,0x68,0x4B,0x88,0x40,0x88,0x57,0xF8,0x48,0x88,0x4B,0xE8,0x5A,0x28,0x4A,0x28,0x4B,0xE8,0x4A,0x28,0x40,0x18,0x00,0x00},/*"阔",184*/
|
||||||
{0x00,0x00,0x3F,0xF0,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0xFF,0xFC,0x02,0x00,0x05,0x00,0x04,0x80,0x08,0x40,0x10,0x20,0x20,0x1C,0xC0,0x08,0x00,0x00},/*"天",185*/
|
{0x00,0x00,0x3F,0xF0,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0xFF,0xFC,0x02,0x00,0x05,0x00,0x04,0x80,0x08,0x40,0x10,0x20,0x20,0x1C,0xC0,0x08,0x00,0x00},/*"天",185*/
|
||||||
{0x04,0x00,0x02,0x00,0x7F,0xF8,0x40,0x10,0x88,0x80,0x10,0x40,0x20,0x20,0x40,0x10,0x3F,0xE0,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0xFF,0xF8,0x00,0x00},/*"空",186*/
|
{0x04,0x00,0x02,0x00,0x7F,0xF8,0x40,0x10,0x88,0x80,0x10,0x40,0x20,0x20,0x40,0x10,0x3F,0xE0,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0xFF,0xF8,0x00,0x00},/*"空",186*/
|
||||||
|
{0x20,0x00,0x1B,0xF8,0x08,0x08,0x40,0x88,0x40,0x88,0x5F,0xE8,0x40,0x88,0x41,0x88,0x42,0x88,0x44,0x88,0x48,0x88,0x43,0x88,0x40,0x28,0x40,0x10,0x00,0x00},/*"闭",187*/
|
||||||
};
|
};
|
||||||
|
|
||||||
const unsigned char Chinese_font_16[][32] = {
|
const unsigned char Chinese_font_16[][32] = {
|
||||||
|
|
@ -1707,6 +1721,7 @@ const unsigned char Chinese_font_16[][32] = {
|
||||||
{0x20,0x00,0x17,0xFC,0x00,0x04,0x48,0x34,0x45,0xC4,0x64,0x44,0x50,0x44,0x51,0xF4,0x44,0x44,0x48,0x44,0x79,0xF4,0x49,0x14,0x49,0x14,0x49,0xF4,0x40,0x04,0x40,0x0C},/*"阔",187*/
|
{0x20,0x00,0x17,0xFC,0x00,0x04,0x48,0x34,0x45,0xC4,0x64,0x44,0x50,0x44,0x51,0xF4,0x44,0x44,0x48,0x44,0x79,0xF4,0x49,0x14,0x49,0x14,0x49,0xF4,0x40,0x04,0x40,0x0C},/*"阔",187*/
|
||||||
{0x00,0x00,0x3F,0xF8,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0xFF,0xFE,0x01,0x00,0x02,0x80,0x02,0x80,0x04,0x40,0x04,0x40,0x08,0x20,0x10,0x10,0x20,0x08,0xC0,0x06},/*"天",188*/
|
{0x00,0x00,0x3F,0xF8,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0xFF,0xFE,0x01,0x00,0x02,0x80,0x02,0x80,0x04,0x40,0x04,0x40,0x08,0x20,0x10,0x10,0x20,0x08,0xC0,0x06},/*"天",188*/
|
||||||
{0x02,0x00,0x01,0x00,0x7F,0xFE,0x40,0x02,0x88,0x24,0x10,0x10,0x20,0x08,0x00,0x00,0x1F,0xF0,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x7F,0xFC,0x00,0x00},/*"空",189*/
|
{0x02,0x00,0x01,0x00,0x7F,0xFE,0x40,0x02,0x88,0x24,0x10,0x10,0x20,0x08,0x00,0x00,0x1F,0xF0,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x7F,0xFC,0x00,0x00},/*"空",189*/
|
||||||
|
{0x20,0x00,0x17,0xFC,0x00,0x04,0x40,0x84,0x40,0x84,0x5F,0xF4,0x40,0x84,0x41,0x84,0x42,0x84,0x44,0x84,0x48,0x84,0x50,0x84,0x42,0x84,0x41,0x04,0x40,0x14,0x40,0x08},/*"闭",190*/
|
||||||
};
|
};
|
||||||
|
|
||||||
const unsigned char Chinese_font_24[][72] = {
|
const unsigned char Chinese_font_24[][72] = {
|
||||||
|
|
@ -1739,6 +1754,7 @@ const unsigned char Chinese_font_24[][72] = {
|
||||||
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x00,0x00,0x3C,0x00,0x00,0x3C,0x00,0x00,0x28,0x00,0x00,0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"认",26*/
|
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x00,0x00,0x3C,0x00,0x00,0x3C,0x00,0x00,0x28,0x00,0x00,0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"认",26*/
|
||||||
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x24,0x00,0x00,0x3C,0x00,0x00,0x14,0x00,0x00,0x2C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"分",27*/
|
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x24,0x00,0x00,0x3C,0x00,0x00,0x14,0x00,0x00,0x2C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"分",27*/
|
||||||
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x3C,0x00,0x00,0x3C,0x00,0x00,0x3C,0x00,0x00,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"钟",28*/
|
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x3C,0x00,0x00,0x3C,0x00,0x00,0x3C,0x00,0x00,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"钟",28*/
|
||||||
|
{0x18,0x00,0x00,0x18,0x00,0x00,0x04,0xFF,0xF8,0x00,0x00,0x08,0x00,0x00,0x08,0x20,0x0C,0x08,0x20,0x0C,0x08,0x20,0x0C,0x08,0x27,0xFF,0xC8,0x20,0x0C,0x08,0x20,0x0C,0x08,0x20,0x1C,0x08,0x20,0x6C,0x08,0x20,0x6C,0x08,0x20,0x8C,0x08,0x23,0x0C,0x08,0x23,0x0C,0x08,0x24,0x0C,0x08,0x20,0x6C,0x08,0x20,0x6C,0x08,0x20,0x10,0x08,0x20,0x00,0x48,0x20,0x00,0x48,0x20,0x00,0x30},/*"闭",29*/
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,15 +7,15 @@ extern const unsigned char gImage_UI0902_BatteryOutline_29x12[];
|
||||||
extern const unsigned char gImage_UI0902_Volume_20x15[600];
|
extern const unsigned char gImage_UI0902_Volume_20x15[600];
|
||||||
extern const unsigned char gImage_UI0902_Bluetooth_11x17[374];
|
extern const unsigned char gImage_UI0902_Bluetooth_11x17[374];
|
||||||
extern const unsigned char gImage_UI0902_BatteryFill_22x9[396];
|
extern const unsigned char gImage_UI0902_BatteryFill_22x9[396];
|
||||||
extern const unsigned char gImage_UI0902_TabSetting_Sel_36x28[2016];
|
extern const unsigned char gImage_UI0902_TabSetting_Sel_40x32[2560];
|
||||||
extern const unsigned char gImage_UI0902_TabSetting_Not_35x28[1960];
|
extern const unsigned char gImage_UI0902_TabSetting_Not_40x32[2560];
|
||||||
extern const unsigned char gImage_UI0902_TabMixer_Sel_35x27[1890];
|
extern const unsigned char gImage_UI0902_TabMixer_Sel_40x32[2560];
|
||||||
extern const unsigned char gImage_UI0902_TabMixer_Not_18x28[1008];
|
extern const unsigned char gImage_UI0902_TabMixer_Not_22x32[1408];
|
||||||
extern const unsigned char gImage_UI0902_TabMixer_Not_35x28[1960];
|
extern const unsigned char gImage_UI0902_TabMixer_Not_35x28[1960];
|
||||||
extern const unsigned char gImage_UI0902_TabMode_Sel_36x28[2016];
|
extern const unsigned char gImage_UI0902_TabMode_Sel_40x32[2560];
|
||||||
extern const unsigned char gImage_UI0902_TabMode_Not_35x29[2030];
|
extern const unsigned char gImage_UI0902_TabMode_Not_40x32[2560];
|
||||||
extern const unsigned char gImage_UI0902_TabBack_Sel_35x27[1890];
|
extern const unsigned char gImage_UI0902_TabBack_Sel_40x32[2560];
|
||||||
extern const unsigned char gImage_UI0902_TabBack_Not_18x28[1008];
|
extern const unsigned char gImage_UI0902_TabBack_Not_22x32[1408];
|
||||||
|
|
||||||
extern const unsigned char gImage_UI0902_BtnMinus_23x22[1012];
|
extern const unsigned char gImage_UI0902_BtnMinus_23x22[1012];
|
||||||
extern const unsigned char gImage_UI0902_BtnPlus_23x22[1012];
|
extern const unsigned char gImage_UI0902_BtnPlus_23x22[1012];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// File: w25q128.c
|
// File: w25q128.c
|
||||||
#include "w25q128.h"
|
#include "w25q128.h"
|
||||||
#include "wk_system.h"
|
#include "wk_system.h"
|
||||||
|
#include "rtthread.h"
|
||||||
|
|
||||||
/* W25Q128 挂在 SPI1 上(PA5=SCK, PA6=MISO, PA7=MOSI),CS 用 PA4 GPIO 控制 */
|
/* W25Q128 挂在 SPI1 上(PA5=SCK, PA6=MISO, PA7=MOSI),CS 用 PA4 GPIO 控制 */
|
||||||
#define W25Q128_SPI SPI1
|
#define W25Q128_SPI SPI1
|
||||||
|
|
@ -82,6 +83,12 @@ static void W25Q128_WaitBusy(void)
|
||||||
W25Q128_ReadWriteByte(W25X_ReadStatusReg1); // 发送读状态寄存器命令
|
W25Q128_ReadWriteByte(W25X_ReadStatusReg1); // 发送读状态寄存器命令
|
||||||
do {
|
do {
|
||||||
status = W25Q128_ReadWriteByte(0xFF); // 循环读取状态字
|
status = W25Q128_ReadWriteByte(0xFF); // 循环读取状态字
|
||||||
|
if (status & 0x01) {
|
||||||
|
SPI_CS_HIGH();
|
||||||
|
rt_thread_mdelay(1);
|
||||||
|
SPI_CS_LOW();
|
||||||
|
W25Q128_ReadWriteByte(W25X_ReadStatusReg1);
|
||||||
|
}
|
||||||
} while (status & 0x01); // 等待BUSY位清零
|
} while (status & 0x01); // 等待BUSY位清零
|
||||||
SPI_CS_HIGH();
|
SPI_CS_HIGH();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,43 @@ void SendVolumeChangeMidiEvent(uint8_t channel,uint8_t CC)
|
||||||
SendMidiDataToDreamDSP(midibuff,3);
|
SendMidiDataToDreamDSP(midibuff,3);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SendBankChangeMidiEvent(uint8_t CC)
|
/* Bank Select:CC0(MSB)=bank,CC32(LSB)=0。Dream 双音色库:0=伴奏,1=主弦 */
|
||||||
|
void SendBankChangeMidiEvent(uint8_t channel, uint8_t bank)
|
||||||
{
|
{
|
||||||
uint8_t midibuff[3] = {0xB0 ,0x00, CC };
|
uint8_t midibuff[3] = {0xB0|(channel & 0x0F) ,0x00, bank };
|
||||||
SendMidiDataToDreamDSP(midibuff,3);
|
SendMidiDataToDreamDSP(midibuff,3);
|
||||||
|
midibuff[1] = 0x20; /* CC32 Bank LSB */
|
||||||
|
midibuff[2] = 0;
|
||||||
|
SendMidiDataToDreamDSP(midibuff,3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 先选 Bank 再 Program Change,避免漏发 Bank Select 落到错误音色库 */
|
||||||
|
void SendProgramWithBankMidiEvent(uint8_t channel, uint8_t bank, uint8_t program)
|
||||||
|
{
|
||||||
|
SendBankChangeMidiEvent(channel, bank);
|
||||||
|
SendProgramChangeMidiEvent(channel, program);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SendMidiDataToDreamDSP(uint8_t* buff, uint16_t cnt)
|
void SendMidiDataToDreamDSP(uint8_t* buff, uint16_t cnt)
|
||||||
{
|
{
|
||||||
USART2_SendData(buff,cnt);
|
USART2_SendData(buff,cnt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define AT32_UID_BASE 0x1FFFF7E8UL
|
||||||
|
|
||||||
|
void BleBuildDeviceName(char *out)
|
||||||
|
{
|
||||||
|
static const char hex[] = "0123456789ABCDEF";
|
||||||
|
const uint32_t *uid = (const uint32_t *)AT32_UID_BASE;
|
||||||
|
uint32_t tail = uid[2] & 0x00FFFFFFu;
|
||||||
|
unsigned i;
|
||||||
|
|
||||||
|
out[0] = 'P';
|
||||||
|
out[1] = 'H';
|
||||||
|
out[2] = 'O';
|
||||||
|
out[3] = 'N';
|
||||||
|
out[4] = '_';
|
||||||
|
for (i = 0; i < 6; i++)
|
||||||
|
out[5 + i] = hex[(tail >> (4 * (5 - i))) & 0xFu];
|
||||||
|
out[BLE_DEVICE_NAME_LEN] = '\0';
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,22 @@
|
||||||
#define __MIDI_SEND_H__
|
#define __MIDI_SEND_H__
|
||||||
|
|
||||||
|
|
||||||
|
/* Dream 音色库 Bank:0=自动伴奏,1=主音色/弦 */
|
||||||
|
#define MIDI_BANK_ACCOMP 0
|
||||||
|
#define MIDI_BANK_STRING 1
|
||||||
|
|
||||||
/* MIDI 发送函数 */
|
/* MIDI 发送函数 */
|
||||||
void SendNoteOnMidiEvent(uint8_t Key, uint8_t OnVal);
|
void SendNoteOnMidiEvent(uint8_t Key, uint8_t OnVal);
|
||||||
void SendNoteOffMidiEvent(uint8_t Key, uint8_t OffVal);
|
void SendNoteOffMidiEvent(uint8_t Key, uint8_t OffVal);
|
||||||
void SendProgramChangeMidiEvent(uint8_t channel, uint8_t CC);
|
void SendProgramChangeMidiEvent(uint8_t channel, uint8_t CC);
|
||||||
void SendBankChangeMidiEvent(uint8_t CC);
|
void SendBankChangeMidiEvent(uint8_t channel, uint8_t bank);
|
||||||
|
void SendProgramWithBankMidiEvent(uint8_t channel, uint8_t bank, uint8_t program);
|
||||||
void SendVolumeChangeMidiEvent(uint8_t channel, uint8_t CC);
|
void SendVolumeChangeMidiEvent(uint8_t channel, uint8_t CC);
|
||||||
void SendMidiDataToDreamDSP(uint8_t* buff, uint16_t cnt);
|
void SendMidiDataToDreamDSP(uint8_t* buff, uint16_t cnt);
|
||||||
void DefaultTask_SendMsg(uint16_t Data1, uint16_t Data2, uint16_t Data3, uint16_t Data4);
|
void DefaultTask_SendMsg(uint16_t Data1, uint16_t Data2, uint16_t Data3, uint16_t Data4);
|
||||||
|
|
||||||
|
/* Unique BLE/BT name from AT32 UID: "PHON_" + 6 hex (last 3 UID bytes). out >= 12. */
|
||||||
|
#define BLE_DEVICE_NAME_LEN 11
|
||||||
|
void BleBuildDeviceName(char *out);
|
||||||
|
|
||||||
#endif /* __MIDI_SEND_H__ */
|
#endif /* __MIDI_SEND_H__ */
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,8 @@ const STRING_MIDI *Chord_Midi_Table[7] =
|
||||||
const STRING_MIDI *OrganChord_Midi_Table[7] =
|
const STRING_MIDI *OrganChord_Midi_Table[7] =
|
||||||
{ OrganMIDI_1,OrganMIDI_2,OrganMIDI_3,OrganMIDI_4,OrganMIDI_5,OrganMIDI_6, OrganMIDI_7 };
|
{ OrganMIDI_1,OrganMIDI_2,OrganMIDI_3,OrganMIDI_4,OrganMIDI_5,OrganMIDI_6, OrganMIDI_7 };
|
||||||
|
|
||||||
const uint8_t Timbre_index[5] = {0x18,0x19,0x1B,0x1E,0x22};
|
/* UI槽位0~4 → MIDI Program Change(Bank1):24尼龙 / 25钢弦 / 27清音 / 30失真 / 33电贝司 */
|
||||||
|
const uint8_t Timbre_index[5] = {0x18, 0x19, 0x1B, 0x1E, 0x21};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -663,7 +663,7 @@
|
||||||
<name>BUILDACTION</name>
|
<name>BUILDACTION</name>
|
||||||
<archiveVersion>1</archiveVersion>
|
<archiveVersion>1</archiveVersion>
|
||||||
<data>
|
<data>
|
||||||
<prebuild></prebuild>
|
<prebuild>python "$PROJ_DIR$\..\..\tools\gen_git_user_fw_ver.py"</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 "$PROJ_DIR$\..\..\tools\gen_git_user_fw_ver.py"</prebuild>
|
||||||
<postbuild></postbuild>
|
<postbuild></postbuild>
|
||||||
</data>
|
</data>
|
||||||
</settings>
|
</settings>
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
/* Auto-generated by tools/pack_extflash_tone_0903.py — do not hand-edit. */
|
/* Auto-generated by tools/pack_extflash_tone_0903.py — do not hand-edit. */
|
||||||
/* W25Q128 absolute offsets. Boot logo display uses UI0902_BOOT_LOGO_ADDR. */
|
/* W25Q128 absolute offsets. Boot logo display uses UI0902_BOOT_LOGO_ADDR. */
|
||||||
/* Tone resource pack has no in-bin version field; release tag = EXTFLASH_TONE_RES_VER. */
|
/* Tone resource pack has no in-bin version field; release tag = EXTFLASH_TONE_RES_VER. */
|
||||||
#define EXTFLASH_TONE_RES_VER "0903"
|
#define EXTFLASH_TONE_RES_VER "0916"
|
||||||
#define EXTFLASH_TONE_RES_VER_MAJOR 0
|
#define EXTFLASH_TONE_RES_VER_MAJOR 0
|
||||||
#define EXTFLASH_TONE_RES_VER_MINOR 9
|
#define EXTFLASH_TONE_RES_VER_MINOR 9
|
||||||
#define EXTFLASH_TONE_RES_VER_PATCH 3
|
#define EXTFLASH_TONE_RES_VER_PATCH 16
|
||||||
|
|
||||||
#define EXTFLASH_LOGO_ADDR 0x00000000UL
|
#define EXTFLASH_LOGO_ADDR 0x00000000UL
|
||||||
#define EXTFLASH_LOGO_SIZE 52080UL
|
#define EXTFLASH_LOGO_SIZE 52080UL
|
||||||
|
|
@ -18,13 +18,13 @@
|
||||||
#define EXTFLASH_CHARGING_H 190
|
#define EXTFLASH_CHARGING_H 190
|
||||||
|
|
||||||
#define EXTFLASH_BIN1_RHYTHM_ADDR 0x0001B8F0UL
|
#define EXTFLASH_BIN1_RHYTHM_ADDR 0x0001B8F0UL
|
||||||
#define EXTFLASH_BIN1_RHYTHM_SIZE 530317UL
|
#define EXTFLASH_BIN1_RHYTHM_SIZE 506746UL
|
||||||
|
|
||||||
#define EXTFLASH_BIN2_SONG_HAITIAN_ADDR 0x0009D07DUL
|
#define EXTFLASH_BIN2_SONG_HAITIAN_ADDR 0x0009D07DUL
|
||||||
#define EXTFLASH_BIN2_SONG_HAITIAN_SIZE 41263UL
|
#define EXTFLASH_BIN2_SONG_HAITIAN_SIZE 41914UL
|
||||||
|
|
||||||
#define EXTFLASH_BIN3_UNIVERSAL_ADDR 0x000A71ACUL
|
#define EXTFLASH_BIN3_UNIVERSAL_ADDR 0x000A7437UL
|
||||||
#define EXTFLASH_BIN3_UNIVERSAL_SIZE 31854UL
|
#define EXTFLASH_BIN3_UNIVERSAL_SIZE 33945UL
|
||||||
|
|
||||||
/* Convenience aliases used by UI ADDRESS */
|
/* Convenience aliases used by UI ADDRESS */
|
||||||
#define FLASH_ADDR_MODE_NORMAL EXTFLASH_BIN1_RHYTHM_ADDR /* 普通 */
|
#define FLASH_ADDR_MODE_NORMAL EXTFLASH_BIN1_RHYTHM_ADDR /* 普通 */
|
||||||
|
|
@ -32,11 +32,11 @@
|
||||||
#define FLASH_ADDR_SONG_HAITIAN EXTFLASH_BIN2_SONG_HAITIAN_ADDR
|
#define FLASH_ADDR_SONG_HAITIAN EXTFLASH_BIN2_SONG_HAITIAN_ADDR
|
||||||
#define FLASH_ADDR_MODE_UNIVERSAL EXTFLASH_BIN3_UNIVERSAL_ADDR /* 万能 */
|
#define FLASH_ADDR_MODE_UNIVERSAL EXTFLASH_BIN3_UNIVERSAL_ADDR /* 万能 */
|
||||||
|
|
||||||
/* Legacy AutoBand bank — not repacked in 0903; left unchanged in firmware */
|
/* Legacy AutoBand bank — not repacked; may overlap 2.bin — do not enable until remapped */
|
||||||
#define FLASH_ADDR_AUTOBAND_LEGACY 0x0009EB5FUL
|
#define FLASH_ADDR_AUTOBAND_LEGACY 0x0009EB5FUL
|
||||||
|
|
||||||
#define EXTFLASH_TONE_PACK_END 0x000AEE1AUL
|
#define EXTFLASH_TONE_PACK_END 0x000AF8D0UL
|
||||||
#define EXTFLASH_TONE_PACK_SIZE 716314UL
|
#define EXTFLASH_TONE_PACK_SIZE 719056UL
|
||||||
#define UI0902_RES_BASE_SAFE_GAP (0x00100000UL - EXTFLASH_TONE_PACK_END)
|
#define UI0902_RES_BASE_SAFE_GAP (0x00100000UL - EXTFLASH_TONE_PACK_END)
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,9 @@ void wk_tmr6_1ms_init(void);
|
||||||
void wk_tmr6_1ms_tick(void);
|
void wk_tmr6_1ms_tick(void);
|
||||||
uint32_t wk_tmr6_tick_get(void);
|
uint32_t wk_tmr6_tick_get(void);
|
||||||
|
|
||||||
|
/* TMR7 1ms:关机键看门狗(优先级高于 TMR6,主循环卡死仍可关) */
|
||||||
|
void wk_tmr7_pwrkey_init(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,9 @@
|
||||||
|
|
||||||
/* private includes ----------------------------------------------------------*/
|
/* private includes ----------------------------------------------------------*/
|
||||||
/* add user code begin private includes */
|
/* add user code begin private includes */
|
||||||
|
/* 关机键看门狗:与 Global.c 解耦声明,避免 int.c 拉全量 includes */
|
||||||
|
void Power_Key_IsrTick(void);
|
||||||
|
void Power_Key_FaultLoop(void);
|
||||||
/* add user code end private includes */
|
/* add user code end private includes */
|
||||||
|
|
||||||
/* private typedef -----------------------------------------------------------*/
|
/* private typedef -----------------------------------------------------------*/
|
||||||
|
|
@ -110,16 +112,7 @@ void NMI_Handler(void)
|
||||||
*/
|
*/
|
||||||
void MemManage_Handler(void)
|
void MemManage_Handler(void)
|
||||||
{
|
{
|
||||||
/* add user code begin MemoryManagement_IRQ 0 */
|
Power_Key_FaultLoop();
|
||||||
|
|
||||||
/* add user code end MemoryManagement_IRQ 0 */
|
|
||||||
/* go to infinite loop when memory manage exception occurs */
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
/* add user code begin W1_MemoryManagement_IRQ 0 */
|
|
||||||
|
|
||||||
/* add user code end W1_MemoryManagement_IRQ 0 */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -129,16 +122,7 @@ void MemManage_Handler(void)
|
||||||
*/
|
*/
|
||||||
void BusFault_Handler(void)
|
void BusFault_Handler(void)
|
||||||
{
|
{
|
||||||
/* add user code begin BusFault_IRQ 0 */
|
Power_Key_FaultLoop();
|
||||||
|
|
||||||
/* add user code end BusFault_IRQ 0 */
|
|
||||||
/* go to infinite loop when bus fault exception occurs */
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
/* add user code begin W1_BusFault_IRQ 0 */
|
|
||||||
|
|
||||||
/* add user code end W1_BusFault_IRQ 0 */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -148,16 +132,7 @@ void BusFault_Handler(void)
|
||||||
*/
|
*/
|
||||||
void UsageFault_Handler(void)
|
void UsageFault_Handler(void)
|
||||||
{
|
{
|
||||||
/* add user code begin UsageFault_IRQ 0 */
|
Power_Key_FaultLoop();
|
||||||
|
|
||||||
/* add user code end UsageFault_IRQ 0 */
|
|
||||||
/* go to infinite loop when usage fault exception occurs */
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
/* add user code begin W1_UsageFault_IRQ 0 */
|
|
||||||
|
|
||||||
/* add user code end W1_UsageFault_IRQ 0 */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -241,6 +216,15 @@ void TMR6_GLOBAL_IRQHandler(void)
|
||||||
//rt_os_tick_callback();
|
//rt_os_tick_callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief TMR7:关机键看门狗(先于伴奏 TMR6 响应,主循环死锁仍可关)
|
||||||
|
*/
|
||||||
|
void TMR7_GLOBAL_IRQHandler(void)
|
||||||
|
{
|
||||||
|
tmr_flag_clear(TMR7, TMR_OVF_FLAG);
|
||||||
|
Power_Key_IsrTick();
|
||||||
|
}
|
||||||
|
|
||||||
/* add user code begin 1 */
|
/* add user code begin 1 */
|
||||||
|
|
||||||
/* add user code end 1 */
|
/* add user code end 1 */
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,8 @@ int main(void)
|
||||||
drv_nvm_init();
|
drv_nvm_init();
|
||||||
TaskInit();
|
TaskInit();
|
||||||
timer_init();
|
timer_init();
|
||||||
|
/* 关机键看门狗:TMR7 + HardFault hook,与主循环/伴奏 ISR 解耦 */
|
||||||
|
Power_Key_WatchdogInit();
|
||||||
InitMenuUI();
|
InitMenuUI();
|
||||||
BSP_HT7178PowerEnable (1);
|
BSP_HT7178PowerEnable (1);
|
||||||
BSP_ChargerEnable(1);
|
BSP_ChargerEnable(1);
|
||||||
|
|
@ -105,6 +107,7 @@ int main(void)
|
||||||
/* add user code begin 3 */
|
/* add user code begin 3 */
|
||||||
Power_Key_Scan();
|
Power_Key_Scan();
|
||||||
AutoPowerOff_Scan();
|
AutoPowerOff_Scan();
|
||||||
|
System_PowerOff_Poll();
|
||||||
LCD_Dump_Poll();
|
LCD_Dump_Poll();
|
||||||
#if !DEBUG_LCD_DUMP
|
#if !DEBUG_LCD_DUMP
|
||||||
app_log_poll();
|
app_log_poll();
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
|
|
||||||
#define BUFF_SIZE 512
|
#define BUFF_SIZE 512
|
||||||
|
|
||||||
/* 纯比较宏:参数必须是普通局部变量(非 volatile),
|
/* ?????????????????????????????? volatile????
|
||||||
避免同一表达式多次访问 volatile 触发 Pa082 且逻辑不稳 */
|
?????????????????? volatile ???? Pa082 ????????? */
|
||||||
#define IS_FULL(head, tail) ((((head) + 1) % BUFF_SIZE) == (tail))
|
#define IS_FULL(head, tail) ((((head) + 1) % BUFF_SIZE) == (tail))
|
||||||
#define IS_EMPTY(head, tail) ((head) == (tail))
|
#define IS_EMPTY(head, tail) ((head) == (tail))
|
||||||
|
|
||||||
/* ==================== 环形队列结构体 ==================== */
|
/* ==================== ???????????? ==================== */
|
||||||
|
|
||||||
/* 一个串口实例的环形队列(收发各一) */
|
/* ????????????????????????????? */
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
uint8_t rx_buff[BUFF_SIZE];
|
uint8_t rx_buff[BUFF_SIZE];
|
||||||
|
|
@ -21,14 +21,14 @@ typedef struct
|
||||||
volatile uint16_t tx_head;
|
volatile uint16_t tx_head;
|
||||||
volatile uint16_t tx_tail;
|
volatile uint16_t tx_tail;
|
||||||
|
|
||||||
usart_type *usart; /* 关联的串口外设(USART2 / UART4) */
|
usart_type *usart; /* ??????????????USART2 / UART4?? */
|
||||||
} uart_ring_t;
|
} uart_ring_t;
|
||||||
|
|
||||||
/* USART2 和 UART4 各一个实例 */
|
/* USART2 ?? UART4 ???????? */
|
||||||
static uart_ring_t uart2;
|
static uart_ring_t uart2;
|
||||||
static uart_ring_t uart4;
|
static uart_ring_t uart4;
|
||||||
|
|
||||||
/* 初始化:关联串口外设(由 wk_usart2_init / wk_uart4_init 调用) */
|
/* ?????????????????????? wk_usart2_init / wk_uart4_init ????? */
|
||||||
void uart_ring_usart2_init(void)
|
void uart_ring_usart2_init(void)
|
||||||
{
|
{
|
||||||
uart2.usart = USART2;
|
uart2.usart = USART2;
|
||||||
|
|
@ -39,19 +39,19 @@ void uart_ring_uart4_init(void)
|
||||||
uart4.usart = UART4;
|
uart4.usart = UART4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 通用入队/出队(带实例参数) ==================== */
|
/* ==================== ??????/???????????????? ==================== */
|
||||||
|
|
||||||
/* 入队(ISR 调用)。先快照 volatile 值再操作,避免 Pa082 */
|
/* ????ISR ???????????? volatile ???????????? Pa082 */
|
||||||
static void ring_rx_push(uart_ring_t *rb, uint8_t c)
|
static void ring_rx_push(uart_ring_t *rb, uint8_t c)
|
||||||
{
|
{
|
||||||
uint16_t h = rb->rx_head;
|
uint16_t h = rb->rx_head;
|
||||||
uint16_t t = rb->rx_tail;
|
uint16_t t = rb->rx_tail;
|
||||||
if(IS_FULL(h, t)) return; /* 满则丢,防止覆盖未读数据 */
|
if(IS_FULL(h, t)) return; /* ???????????????????? */
|
||||||
rb->rx_buff[h] = c;
|
rb->rx_buff[h] = c;
|
||||||
rb->rx_head = (h + 1) % BUFF_SIZE;
|
rb->rx_head = (h + 1) % BUFF_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 出队(业务线程调用) */
|
/* ??????????????? */
|
||||||
static uint8_t ring_rx_pop(uart_ring_t *rb, uint8_t *c)
|
static uint8_t ring_rx_pop(uart_ring_t *rb, uint8_t *c)
|
||||||
{
|
{
|
||||||
uint16_t h = rb->rx_head;
|
uint16_t h = rb->rx_head;
|
||||||
|
|
@ -62,65 +62,81 @@ static uint8_t ring_rx_pop(uart_ring_t *rb, uint8_t *c)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 写入 tx 队列(调用前应已关中断,防止 ISR 同时读) */
|
/* ???? tx ??????????????????????? ISR ?????? */
|
||||||
static void ring_tx_write(uart_ring_t *rb, uint8_t *data, uint16_t len)
|
static void ring_tx_write(uart_ring_t *rb, uint8_t *data, uint16_t len)
|
||||||
{
|
{
|
||||||
uint16_t h = rb->tx_head;
|
uint16_t h = rb->tx_head;
|
||||||
uint16_t t = rb->tx_tail;
|
uint16_t t = rb->tx_tail;
|
||||||
for(uint16_t i = 0; i < len; i++)
|
for(uint16_t i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
if(IS_FULL(h, t)) break; /* 用 tx 判满 */
|
if(IS_FULL(h, t)) break; /* ?? tx ???? */
|
||||||
rb->tx_buff[h] = data[i];
|
rb->tx_buff[h] = data[i];
|
||||||
h = (h + 1) % BUFF_SIZE;
|
h = (h + 1) % BUFF_SIZE;
|
||||||
}
|
}
|
||||||
rb->tx_head = h; /* 最后一次性写回 volatile */
|
rb->tx_head = h; /* ???????????? volatile */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 发送接口(带串口参数) ==================== */
|
/* ==================== ?????????????????? ==================== */
|
||||||
|
|
||||||
/* 通用发送:关中断 → 写队列 → 开 TDBE 中断触发发送 */
|
/* ?????????????? ?? ?????? ?? ?? TDBE ??????????? */
|
||||||
static void uart_ring_send(uart_ring_t *rb, uint8_t *data, uint16_t len)
|
static void uart_ring_send(uart_ring_t *rb, uint8_t *data, uint16_t len)
|
||||||
{
|
{
|
||||||
rt_base_t level = rt_hw_interrupt_disable();
|
rt_base_t level = rt_hw_interrupt_disable();
|
||||||
|
|
||||||
ring_tx_write(rb, data, len);
|
ring_tx_write(rb, data, len);
|
||||||
usart_interrupt_enable(rb->usart, USART_TDBE_INT, TRUE); /* 开发送中断 */
|
usart_interrupt_enable(rb->usart, USART_TDBE_INT, TRUE); /* ?????????? */
|
||||||
|
|
||||||
rt_hw_interrupt_enable(level);
|
rt_hw_interrupt_enable(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* USART2 发送(对外,midi_send.c 等在用) */
|
/* USART2 ?????????midi_send.c ??????? */
|
||||||
void USART2_SendData(uint8_t *data, uint16_t len)
|
void USART2_SendData(uint8_t *data, uint16_t len)
|
||||||
{
|
{
|
||||||
uart_ring_send(&uart2, data, len);
|
uart_ring_send(&uart2, data, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* UART4 发送(对外) */
|
/* UART4 ????????? */
|
||||||
void USART4_SendData(uint8_t *data, uint16_t len)
|
void USART4_SendData(uint8_t *data, uint16_t len)
|
||||||
{
|
{
|
||||||
uart_ring_send(&uart4, data, len);
|
uart_ring_send(&uart4, data, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* USART2 接收出队(对外) */
|
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 ???????????? */
|
||||||
uint8_t USART2_RxPop(uint8_t *c)
|
uint8_t USART2_RxPop(uint8_t *c)
|
||||||
{
|
{
|
||||||
return ring_rx_pop(&uart2, c);
|
return ring_rx_pop(&uart2, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* UART4 接收出队(对外) */
|
/* UART4 ???????????? */
|
||||||
uint8_t UART4_RxPop(uint8_t *c)
|
uint8_t UART4_RxPop(uint8_t *c)
|
||||||
{
|
{
|
||||||
return ring_rx_pop(&uart4, c);
|
return ring_rx_pop(&uart4, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 串口中断 ==================== */
|
/* ==================== ???????? ==================== */
|
||||||
|
|
||||||
static void uart_ring_isr(uart_ring_t *rb)
|
static void uart_ring_isr(uart_ring_t *rb)
|
||||||
{
|
{
|
||||||
/* 接收:收到字节 → 入 rx 队列 */
|
|
||||||
if(usart_flag_get(rb->usart, USART_RDBF_FLAG) == SET)
|
if(usart_flag_get(rb->usart, USART_RDBF_FLAG) == SET)
|
||||||
{
|
{
|
||||||
uint8_t data = usart_data_receive(rb->usart); /* 读数据(自动清 RDBF) */
|
uint8_t data = usart_data_receive(rb->usart);
|
||||||
ring_rx_push(rb, data);
|
ring_rx_push(rb, data);
|
||||||
if(rb->usart == UART4)
|
if(rb->usart == UART4)
|
||||||
{
|
{
|
||||||
|
|
@ -128,7 +144,6 @@ static void uart_ring_isr(uart_ring_t *rb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 发送:tx 队列有数据 → 发一个字节;发完 → 关 TDBE 中断 */
|
|
||||||
if(usart_flag_get(rb->usart, USART_TDBE_FLAG) == SET)
|
if(usart_flag_get(rb->usart, USART_TDBE_FLAG) == SET)
|
||||||
{
|
{
|
||||||
uint16_t h = rb->tx_head;
|
uint16_t h = rb->tx_head;
|
||||||
|
|
@ -140,11 +155,10 @@ static void uart_ring_isr(uart_ring_t *rb)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
usart_interrupt_enable(rb->usart, USART_TDBE_INT, FALSE); /* 发完关中断 */
|
usart_interrupt_enable(rb->usart, USART_TDBE_INT, FALSE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 错误处理:清溢出标志,防止反复进中断卡死 */
|
|
||||||
if(usart_flag_get(rb->usart, USART_ROERR_FLAG) == SET)
|
if(usart_flag_get(rb->usart, USART_ROERR_FLAG) == SET)
|
||||||
usart_flag_clear(rb->usart, USART_ROERR_FLAG);
|
usart_flag_clear(rb->usart, USART_ROERR_FLAG);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -174,3 +174,16 @@ void wk_tmr6_1ms_init(void)
|
||||||
tmr_counter_enable(TMR6, TRUE);
|
tmr_counter_enable(TMR6, TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief TMR7 1ms:仅扫关机键(抢占优先级 1,高于 TMR6=5 / 伴奏库)
|
||||||
|
*/
|
||||||
|
void wk_tmr7_pwrkey_init(void)
|
||||||
|
{
|
||||||
|
crm_periph_clock_enable(CRM_TMR7_PERIPH_CLOCK, TRUE);
|
||||||
|
tmr_base_init(TMR7, 999, 239);
|
||||||
|
tmr_flag_clear(TMR7, TMR_OVF_FLAG);
|
||||||
|
tmr_interrupt_enable(TMR7, TMR_OVF_INT, TRUE);
|
||||||
|
nvic_irq_enable(TMR7_GLOBAL_IRQn, 1, 0);
|
||||||
|
tmr_counter_enable(TMR7, TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "includes.h"
|
#include "includes.h"
|
||||||
|
#include "git_user_fw_ver.h"
|
||||||
|
|
||||||
|
|
||||||
// Э<><D0AD>֡ͷ/֡β
|
// Э<><D0AD>֡ͷ/֡β
|
||||||
|
|
@ -25,7 +26,7 @@ typedef enum
|
||||||
UART4_RCV_BUFF_MIDIEND
|
UART4_RCV_BUFF_MIDIEND
|
||||||
} UART4_RCV_StatusType;
|
} UART4_RCV_StatusType;
|
||||||
|
|
||||||
#define UART4_PROCESS_BUFF_SIZE 32
|
#define UART4_PROCESS_BUFF_SIZE 64 /* 02 04 full chord-map frame is 47 bytes */
|
||||||
|
|
||||||
// UART4<54><34><EFBFBD><EFBFBD>ȫ<EFBFBD>ֱ<EFBFBD><D6B1><EFBFBD>
|
// UART4<54><34><EFBFBD><EFBFBD>ȫ<EFBFBD>ֱ<EFBFBD><D6B1><EFBFBD>
|
||||||
static UART4_RCV_StatusType UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
static UART4_RCV_StatusType UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
||||||
|
|
@ -48,22 +49,25 @@ typedef struct
|
||||||
// ==============================
|
// ==============================
|
||||||
// ָ<><EFBFBD><EEB4A6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȫ<EFBFBD><C8AB> static<69><63>
|
// ָ<><EFBFBD><EEB4A6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȫ<EFBFBD><C8AB> static<69><63>
|
||||||
// ==============================
|
// ==============================
|
||||||
//static void handleDevDisconnect(uint8_t *data);
|
static void handleDevDisconnect(uint8_t *data);
|
||||||
//static void handleDevConnect(uint8_t *data);
|
static void handleDevConnect(uint8_t *data);
|
||||||
//static void handleDevName(uint8_t *data);
|
static void handleDevName(uint8_t *data);
|
||||||
//static void handleFwMainVer(uint8_t *data);
|
static void handleFwMainVer(uint8_t *data);
|
||||||
//static void handleSoundVer(uint8_t *data);
|
static void handleSoundVer(uint8_t *data);
|
||||||
//static void handleUIVer(uint8_t *data);
|
static void handleUIVer(uint8_t *data);
|
||||||
//static void handleOtherInfo(uint8_t *data);
|
static void handleOtherInfo(uint8_t *data);
|
||||||
//static void handleDevCode(uint8_t *data);
|
static void handleDevCode(uint8_t *data);
|
||||||
//static void handleAutoPowerOff(uint8_t *data);
|
static void handleAutoPowerOff(uint8_t *data);
|
||||||
|
static void handleUserFwVer(uint8_t *data);
|
||||||
|
static void handleAutoPowerOffSet(uint8_t *data);
|
||||||
|
static void handleFwCode(uint8_t *data);
|
||||||
|
|
||||||
static void handleReadRhythmMap(uint8_t *data);
|
static void handleReadRhythmMap(uint8_t *data);
|
||||||
static void handlePitchOffset(uint8_t *data);
|
static void handlePitchOffset(uint8_t *data);
|
||||||
static void handleChordOffset(uint8_t *data);
|
static void handleChordOffset(uint8_t *data);
|
||||||
//static void handleResetChordMap(uint8_t *data);
|
static void handleResetChordMap(uint8_t *data);
|
||||||
|
|
||||||
//static void handleRhythmStyle(uint8_t *data);
|
static void handleRhythmStyle(uint8_t *data);
|
||||||
static void handleStringTimbre(uint8_t *data);
|
static void handleStringTimbre(uint8_t *data);
|
||||||
static void handleBPM(uint8_t *data);
|
static void handleBPM(uint8_t *data);
|
||||||
static void handleTranspose(uint8_t *data);
|
static void handleTranspose(uint8_t *data);
|
||||||
|
|
@ -75,9 +79,8 @@ static void handleLED3(uint8_t *data);
|
||||||
static void handleLED4(uint8_t *data);
|
static void handleLED4(uint8_t *data);
|
||||||
static void handleLED5(uint8_t *data);
|
static void handleLED5(uint8_t *data);
|
||||||
static void handleLED6(uint8_t *data);
|
static void handleLED6(uint8_t *data);
|
||||||
//static void handleStopPlay(uint8_t *data);
|
|
||||||
|
|
||||||
//static void handleDeviceReset(uint8_t *data);
|
static void handleDeviceReset(uint8_t *data);
|
||||||
|
|
||||||
static void handleIntro(uint8_t *data);
|
static void handleIntro(uint8_t *data);
|
||||||
static void handleInterlude(uint8_t *data);
|
static void handleInterlude(uint8_t *data);
|
||||||
|
|
@ -98,52 +101,54 @@ static void handleSectionD(uint8_t *data);
|
||||||
*************************************************/
|
*************************************************/
|
||||||
static const BLE_SysExCmdItem bleSysExCmdTable[] =
|
static const BLE_SysExCmdItem bleSysExCmdTable[] =
|
||||||
{
|
{
|
||||||
// //======== 1. <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ 0x01 ========
|
/*======== 1. device info 0x01 ========*/
|
||||||
// {{0x01, 0x00}, handleDevDisconnect}, // <20>Ͽ<EFBFBD><CFBF>豸
|
{{0x01, 0x00}, handleDevDisconnect}, /* disconnect */
|
||||||
// {{0x01, 0x01}, handleDevConnect}, // <20><><EFBFBD><EFBFBD><EFBFBD>豸
|
{{0x01, 0x01}, handleDevConnect}, /* connect */
|
||||||
// {{0x01, 0x02}, handleDevName}, // <20><>ȡ<EFBFBD>豸<EFBFBD><E8B1B8>
|
{{0x01, 0x02}, handleDevName}, /* device name */
|
||||||
// {{0x01, 0x03}, handleFwMainVer}, // <20>̼<EFBFBD><CCBC><EFBFBD><EFBFBD>汾
|
{{0x01, 0x03}, handleFwMainVer}, /* fw main version */
|
||||||
// {{0x01, 0x04}, handleSoundVer}, // <20><>Դ<EFBFBD>汾
|
{{0x01, 0x04}, handleSoundVer}, /* sound version */
|
||||||
// {{0x01, 0x05}, handleUIVer}, // UI<55>汾
|
{{0x01, 0x05}, handleUIVer}, /* UI version */
|
||||||
// {{0x01, 0x06}, handleOtherInfo}, // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
|
{{0x01, 0x06}, handleOtherInfo}, /* other info */
|
||||||
// {{0x01, 0x07}, handleDevCode}, // <20>豸<EFBFBD><E8B1B8><EFBFBD><EFBFBD>
|
{{0x01, 0x07}, handleDevCode}, /* device code (96bit UID) */
|
||||||
// {{0x01, 0x0A}, handleAutoPowerOff}, // <20>Զ<EFBFBD><D4B6>ػ<EFBFBD>
|
{{0x01, 0x0A}, handleAutoPowerOff}, /* auto power-off read (minutes) */
|
||||||
//
|
{{0x01, 0x0C}, handleUserFwVer}, /* user fw version */
|
||||||
// //======== 2. ӳ<><D3B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 0x02 ========
|
{{0x01, 0x11}, handleAutoPowerOffSet}, /* auto power-off set (minutes) */
|
||||||
{{0x02, 0x01}, handleReadRhythmMap}, // <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD>/<2F><><EFBFBD><EFBFBD>ӳ<EFBFBD><D3B3><EFBFBD>
|
{{0x01, 0x0F}, handleFwCode}, /* fw code (MIDI/BLE-safe; replaces 01 FF) */
|
||||||
{{0x02, 0x02}, handlePitchOffset}, // Pitchƫ<68><C6AB>
|
{{0x01, 0xFF}, handleFwCode}, /* fw code alias (raw UART only; 0xFF illegal in BLE-MIDI SysEx) */
|
||||||
{{0x02, 0x03}, handleChordOffset}, // Chordƫ<64><C6AB>
|
|
||||||
// {{0x02, 0x04}, handleResetChordMap}, // <20><><EFBFBD>ú<EFBFBD><C3BA><EFBFBD>ӳ<EFBFBD><D3B3>Ĭ<EFBFBD><C4AC>ֵ
|
|
||||||
|
|
||||||
//======== 3. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>д 0x03 ========
|
/*======== 2. chord map 0x02 ========*/
|
||||||
// {{0x03, 0x04}, handleRhythmStyle}, // <20><>ȡ/<2F><><EFBFBD>ý<EFBFBD><C3BD><EFBFBD><EFBFBD><EFBFBD>
|
{{0x02, 0x01}, handleReadRhythmMap}, /* read chord/pitch map */
|
||||||
{{0x03, 0x05}, handleStringTimbre}, // <20><>ȡ/<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɫ
|
{{0x02, 0x02}, handlePitchOffset}, /* pitch offset */
|
||||||
{{0x03, 0x06}, handleBPM}, // <20><>ȡ/<2F><><EFBFBD><EFBFBD>BPM
|
{{0x02, 0x03}, handleChordOffset}, /* chord offset */
|
||||||
{{0x03, 0x07}, handleTranspose}, // <20><>ȡ/<2F><><EFBFBD><EFBFBD><EFBFBD>Ƶ<EFBFBD>
|
{{0x02, 0x04}, handleResetChordMap}, /* write whole map (default reset) */
|
||||||
|
|
||||||
//======== 4. <20><><EFBFBD><EFBFBD>/LED<45><44><EFBFBD><EFBFBD> 0x04 ========
|
/*======== 3. guitar params 0x03 ========*/
|
||||||
{{0x04, 0x00}, handleLED0}, // <20><>1<EFBFBD><31>LED
|
{{0x03, 0x04}, handleRhythmStyle}, /* rhythm style r/w + user list */
|
||||||
{{0x04, 0x01}, handleLED1}, // <20><>2<EFBFBD><32>LED
|
{{0x03, 0x05}, handleStringTimbre}, /* string timbre r/w */
|
||||||
{{0x04, 0x02}, handleLED2}, // <20><>3<EFBFBD><33>LED
|
{{0x03, 0x06}, handleBPM}, /* BPM r/w */
|
||||||
{{0x04, 0x03}, handleLED3}, // <20><>4<EFBFBD><34>LED
|
{{0x03, 0x07}, handleTranspose}, /* transpose r/w */
|
||||||
{{0x04, 0x04}, handleLED4}, // <20><>5<EFBFBD><35>LED
|
|
||||||
{{0x04, 0x05}, handleLED5}, // <20><>6<EFBFBD><36>LED
|
|
||||||
{{0x04, 0x06}, handleLED6}, // <20><>7<EFBFBD><37>LED
|
|
||||||
// {{0x04, 0x07}, handleStopPlay}, // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
|
||||||
|
|
||||||
// //======== 5. <20><>λ/<2F>ػ<EFBFBD> 0x05 ========
|
/*======== 4. play / LED 0x04 ========*/
|
||||||
// {{0x05, 0x00}, handleDeviceReset}, // <20>豸<EFBFBD><E8B1B8>λ
|
{{0x04, 0x00}, handleLED0}, /* LED 1 */
|
||||||
//
|
{{0x04, 0x01}, handleLED1}, /* LED 2 */
|
||||||
// //======== 6. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת 0x06 ========
|
{{0x04, 0x02}, handleLED2}, /* LED 3 */
|
||||||
{{0x06, 0x01}, handleIntro}, // ǰ<><C7B0>
|
{{0x04, 0x03}, handleLED3}, /* LED 4 */
|
||||||
{{0x06, 0x02}, handleInterlude}, // <20><><EFBFBD><EFBFBD>
|
{{0x04, 0x04}, handleLED4}, /* LED 5 */
|
||||||
{{0x06, 0x03}, handleOutro}, // β<><CEB2>
|
{{0x04, 0x05}, handleLED5}, /* LED 6 */
|
||||||
{{0x04, 0x07}, handleEnd}, // end
|
{{0x04, 0x06}, handleLED6}, /* LED 7 */
|
||||||
{{0x06, 0x05}, handleSectionA}, // A<><41>
|
{{0x04, 0x07}, handleEnd}, /* end / stop play */
|
||||||
{{0x06, 0x06}, handleSectionB}, // B<><42>
|
|
||||||
{{0x06, 0x07}, handleSectionC}, // C<><43>
|
|
||||||
{{0x06, 0x08}, handleSectionD}, // D<><44>
|
|
||||||
|
|
||||||
|
/*======== 5. reset / power off 0x05 ========*/
|
||||||
|
{{0x05, 0x00}, handleDeviceReset}, /* device reset -> soft power off */
|
||||||
|
|
||||||
|
/*======== 6. section jump 0x06 ========*/
|
||||||
|
{{0x06, 0x01}, handleIntro}, /* intro */
|
||||||
|
{{0x06, 0x02}, handleInterlude}, /* interlude */
|
||||||
|
{{0x06, 0x03}, handleOutro}, /* outro */
|
||||||
|
{{0x06, 0x05}, handleSectionA}, /* section A */
|
||||||
|
{{0x06, 0x06}, handleSectionB}, /* section B */
|
||||||
|
{{0x06, 0x07}, handleSectionC}, /* section C */
|
||||||
|
{{0x06, 0x08}, handleSectionD}, /* section D */
|
||||||
};
|
};
|
||||||
|
|
||||||
// ָ<><D6B8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
// ָ<><D6B8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
|
@ -158,14 +163,16 @@ static const BLE_SysExCmdItem bleSysExCmdTable[] =
|
||||||
*/
|
*/
|
||||||
void processBLESysEXData(uint8_t* data, uint8_t cnt)
|
void processBLESysEXData(uint8_t* data, uint8_t cnt)
|
||||||
{
|
{
|
||||||
//ResetAutoPowerCount();
|
// ֡У<D6A1><D0A3>
|
||||||
// <20><><EFBFBD><EFBFBD>֡У<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]);
|
||||||
|
|
||||||
// <20><>ȡ <20><>ָ<EFBFBD><D6B8> + <20><>ָ<EFBFBD><D6B8>
|
// ȡ<EFBFBD><EFBFBD>ָ<EFBFBD><EFBFBD> + <20><>ָ<EFBFBD><D6B8>
|
||||||
uint8_t cmd[2] = {data[2], data[3]};
|
uint8_t cmd[2] = {data[2], data[3]};
|
||||||
|
|
||||||
// <20><><EFBFBD><EFBFBD>ָ<EFBFBD><D6B8><EFBFBD>ƥ<EFBFBD><C6A5>
|
// <20><><EFBFBD><EFBFBD>ָ<EFBFBD><D6B8><EFBFBD>ƥ<EFBFBD><C6A5>
|
||||||
|
|
@ -195,6 +202,7 @@ void UART4_Data_Process(volatile uint8_t* data)
|
||||||
last_tick = now;
|
last_tick = now;
|
||||||
memset(UART4_Process_Buff, 0, sizeof(UART4_Process_Buff));
|
memset(UART4_Process_Buff, 0, sizeof(UART4_Process_Buff));
|
||||||
UART4_RCV_cnt = 0;
|
UART4_RCV_cnt = 0;
|
||||||
|
UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (UART4_RCV_Status)
|
switch (UART4_RCV_Status)
|
||||||
|
|
@ -251,7 +259,6 @@ void UART4_Data_Process(volatile uint8_t* data)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// <20>Ƿ<EFBFBD><C7B7>ֽڣ<D6BD><DAA3><EFBFBD>λ
|
|
||||||
UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -346,8 +353,9 @@ static void handleTranspose(uint8_t *data)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
{
|
{
|
||||||
uint8_t ReturnTranspose[8] = {0xF0,0x60,0x03,0x07,0x00,0x00,0x00,0xF7};
|
/* doc: F0 60 03 07 00 <val> F7 (7 bytes total) */
|
||||||
ReturnTranspose[5] = mGuiData[GUI_TRANSPOSE].Current;
|
uint8_t ReturnTranspose[7] = {0xF0,0x60,0x03,0x07,0x00,0x00,0xF7};
|
||||||
|
ReturnTranspose[5] = (uint8_t)mGuiData[GUI_TRANSPOSE].Current;
|
||||||
USART4_SendData(ReturnTranspose,sizeof(ReturnTranspose));
|
USART4_SendData(ReturnTranspose,sizeof(ReturnTranspose));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -472,25 +480,42 @@ static void handleLED6(uint8_t *data)
|
||||||
|
|
||||||
static void handlePitchOffset(uint8_t *data)
|
static void handlePitchOffset(uint8_t *data)
|
||||||
{
|
{
|
||||||
BL_Sendmsg(MSG_ID_PITCH_OFFSET, data[4]+1, data[5], 0);
|
uint8_t key = (uint8_t)(data[4] + 1u);
|
||||||
|
if (key < 1u || key > 21u)
|
||||||
|
return;
|
||||||
|
BL_Sendmsg(MSG_ID_PITCH_OFFSET, key, data[5], 0);
|
||||||
}
|
}
|
||||||
static void handleChordOffset(uint8_t *data)
|
static void handleChordOffset(uint8_t *data)
|
||||||
{
|
{
|
||||||
BL_Sendmsg(MSG_ID_CHORD_OFFSET, data[4]+1, data[5], 0);
|
uint8_t key = (uint8_t)(data[4] + 1u);
|
||||||
|
if (key < 1u || key > 21u)
|
||||||
|
return;
|
||||||
|
BL_Sendmsg(MSG_ID_CHORD_OFFSET, key, data[5], 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
extern CHORD_TYPE_INDEX chord_type_index_map[22];
|
extern CHORD_TYPE_INDEX chord_type_index_map[22];
|
||||||
static void handleReadRhythmMap(uint8_t *data)
|
static void handleReadRhythmMap(uint8_t *data)
|
||||||
{
|
{
|
||||||
uint8_t ReadChordMap[47] = {0xF0, 0x60, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7};
|
/* 静态缓冲:禁止在 TaskBTRecv 小栈上再开 47B */
|
||||||
|
static uint8_t ReadChordMap[47];
|
||||||
uint8_t chord_map = 0;
|
uint8_t chord_map = 0;
|
||||||
for(uint8_t i = 1;i < 22;i ++)
|
uint8_t i;
|
||||||
|
|
||||||
|
(void)data;
|
||||||
|
memset(ReadChordMap, 0, sizeof(ReadChordMap));
|
||||||
|
ReadChordMap[0] = 0xF0;
|
||||||
|
ReadChordMap[1] = 0x60;
|
||||||
|
ReadChordMap[2] = 0x02;
|
||||||
|
ReadChordMap[3] = 0x01;
|
||||||
|
ReadChordMap[46] = 0xF7;
|
||||||
|
|
||||||
|
for (i = 1; i < 22; i++)
|
||||||
{
|
{
|
||||||
ReadChordMap[3+i] = chord_type_index_map[i].PitchOffset;
|
ReadChordMap[3 + i] = chord_type_index_map[i].PitchOffset;
|
||||||
}
|
}
|
||||||
for(uint8_t i = 25;i < 46;i ++)
|
for (i = 25; i < 46; i++)
|
||||||
{
|
{
|
||||||
switch(chord_type_index_map[i-24].type)
|
switch (chord_type_index_map[i - 24].type)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
chord_map = 0;
|
chord_map = 0;
|
||||||
|
|
@ -519,10 +544,13 @@ static void handleReadRhythmMap(uint8_t *data)
|
||||||
case 8:
|
case 8:
|
||||||
chord_map = 11;
|
chord_map = 11;
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
chord_map = 0;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
ReadChordMap[i] = chord_map;
|
ReadChordMap[i] = chord_map;
|
||||||
}
|
}
|
||||||
USART4_SendData(ReadChordMap,sizeof(ReadChordMap));
|
USART4_SendData(ReadChordMap, sizeof(ReadChordMap));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handleIntro(uint8_t *data)
|
static void handleIntro(uint8_t *data)
|
||||||
|
|
@ -546,7 +574,7 @@ static void handleEnd(uint8_t *data)
|
||||||
// TM1629D_AllLedOn(LED_COLOR_G);
|
// TM1629D_AllLedOn(LED_COLOR_G);
|
||||||
// TM1629D_UpdateDisplay(0);
|
// TM1629D_UpdateDisplay(0);
|
||||||
// osMutexRelease(Tm1629Mutex);
|
// osMutexRelease(Tm1629Mutex);
|
||||||
BL_Sendmsg(MSG_ID_ADCIN1KEY, 1, 0, 0);
|
BL_Sendmsg(MSG_ID_ADCIN1KEY, 2, 0, 0); /* End -> Postamble(1), distinct from Outro(key=1) */
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handleSectionA(uint8_t *data)
|
static void handleSectionA(uint8_t *data)
|
||||||
|
|
@ -570,3 +598,270 @@ static void BL_Sendmsg(uint16_t Data1, uint16_t Data2, uint16_t Data3, uint16_t
|
||||||
{
|
{
|
||||||
BLTask_Sendmsg(Data1, Data2, Data3, Data4);
|
BLTask_Sendmsg(Data1, Data2, Data3, Data4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================================================================
|
||||||
|
* Appended protocol handlers (per Doc/指令测试.docx)
|
||||||
|
* ==================================================================== */
|
||||||
|
|
||||||
|
/*-------- 1. device info 0x01 --------*/
|
||||||
|
#define AT32_UID_BASE 0x1FFFF7E8UL /* 96-bit unique device ID */
|
||||||
|
|
||||||
|
/* 01 04 音源版本:YY.M.D = 音色资源日期(Dream 无版本查询,发版时手改)
|
||||||
|
* 26.9.8 → 2026-09-08(Doc/音色文件/0908) */
|
||||||
|
static const uint8_t SOUND_VER[4] = {26, 9, 8, 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)
|
||||||
|
{
|
||||||
|
uint8_t resp[6] = {0xF0, 0x60, 0x01, 0x00, 0x01, 0xF7};
|
||||||
|
(void)data;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleDevConnect(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t resp[6] = {0xF0, 0x60, 0x01, 0x01, 0x01, 0xF7};
|
||||||
|
(void)data;
|
||||||
|
ResetAutoPowerCount();
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleDevName(uint8_t *data)
|
||||||
|
{
|
||||||
|
static char name[BLE_DEVICE_NAME_LEN + 1];
|
||||||
|
static uint8_t resp[4 + BLE_DEVICE_NAME_LEN + 1]; /* head4 + name + F7 */
|
||||||
|
(void)data;
|
||||||
|
BleBuildDeviceName(name);
|
||||||
|
resp[0] = 0xF0; resp[1] = 0x60; resp[2] = 0x01; resp[3] = 0x02;
|
||||||
|
memcpy(&resp[4], name, BLE_DEVICE_NAME_LEN);
|
||||||
|
resp[4 + BLE_DEVICE_NAME_LEN] = 0xF7;
|
||||||
|
USART4_SendData(resp, (uint16_t)sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleFwMainVer(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t resp[9] = {0xF0, 0x60, 0x01, 0x03, Version[0], Version[1], Version[2], 0x01, 0xF7};
|
||||||
|
(void)data;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleSoundVer(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t resp[9] = {0xF0, 0x60, 0x01, 0x04, SOUND_VER[0], SOUND_VER[1], SOUND_VER[2], SOUND_VER[3], 0xF7};
|
||||||
|
(void)data;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleUIVer(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t resp[9] = {0xF0, 0x60, 0x01, 0x05, UI_VER[0], UI_VER[1], UI_VER[2], UI_VER[3], 0xF7};
|
||||||
|
(void)data;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleOtherInfo(uint8_t *data)
|
||||||
|
{
|
||||||
|
/* 01 06: reserved — keep 8 zero bytes until product defines fields */
|
||||||
|
uint8_t resp[13] = {0xF0, 0x60, 0x01, 0x06, 0, 0, 0, 0, 0, 0, 0, 0, 0xF7};
|
||||||
|
(void)data;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleDevCode(uint8_t *data)
|
||||||
|
{
|
||||||
|
/* 96-bit UID as 24 hex ASCII — SysEx data must stay 7-bit for BLE-MIDI */
|
||||||
|
static const char hex[] = "0123456789ABCDEF";
|
||||||
|
uint8_t resp[29];
|
||||||
|
const uint8_t *uid = (const uint8_t *)AT32_UID_BASE;
|
||||||
|
uint8_t i;
|
||||||
|
(void)data;
|
||||||
|
resp[0] = 0xF0;
|
||||||
|
resp[1] = 0x60;
|
||||||
|
resp[2] = 0x01;
|
||||||
|
resp[3] = 0x07;
|
||||||
|
for (i = 0; i < 12u; i++)
|
||||||
|
{
|
||||||
|
resp[4u + 2u * i] = (uint8_t)hex[uid[i] >> 4];
|
||||||
|
resp[4u + 2u * i + 1u] = (uint8_t)hex[uid[i] & 0x0Fu];
|
||||||
|
}
|
||||||
|
resp[28] = 0xF7;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleAutoPowerOff(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t resp[6] = {0xF0, 0x60, 0x01, 0x0A, 0x00, 0xF7};
|
||||||
|
(void)data;
|
||||||
|
resp[4] = AutoCloseTime;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleUserFwVer(uint8_t *data)
|
||||||
|
{
|
||||||
|
/* 用户固件版本 = "{branch}_{short6}"[+ '*'],如 develop_0aedb4(ASCII,BLE-MIDI 安全) */
|
||||||
|
uint8_t resp[4u + GIT_BUILD_ID_LEN + 1u];
|
||||||
|
(void)data;
|
||||||
|
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 */
|
||||||
|
static void handleAutoPowerOffSet(uint8_t *data)
|
||||||
|
{
|
||||||
|
NvmParam_Type *nvm = drv_nvm_param_ptr();
|
||||||
|
uint8_t resp[6] = {0xF0, 0x60, 0x01, 0x11, 0x00, 0xF7};
|
||||||
|
AutoCloseTime = data[4];
|
||||||
|
if (nvm->param.AutoCloseTime != AutoCloseTime)
|
||||||
|
{
|
||||||
|
nvm->param.AutoCloseTime = AutoCloseTime;
|
||||||
|
drv_nvm_save_to_flash();
|
||||||
|
}
|
||||||
|
ResetAutoPowerCount();
|
||||||
|
resp[4] = AutoCloseTime;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleFwCode(uint8_t *data)
|
||||||
|
{
|
||||||
|
/* 固件编码: 请求 01 0F(推荐)或 01 FF(仅裸串口);应答统一 01 0F + ASCII,避免 BLE-MIDI 吃掉 0xFF */
|
||||||
|
uint8_t resp[16];
|
||||||
|
uint8_t len = (uint8_t)strlen(fwname);
|
||||||
|
(void)data;
|
||||||
|
if (len > 10u)
|
||||||
|
len = 10u;
|
||||||
|
resp[0] = 0xF0;
|
||||||
|
resp[1] = 0x60;
|
||||||
|
resp[2] = 0x01;
|
||||||
|
resp[3] = 0x0F;
|
||||||
|
memcpy(&resp[4], fwname, len);
|
||||||
|
resp[4 + len] = 0xF7;
|
||||||
|
USART4_SendData(resp, (uint16_t)(4u + len + 1u));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*-------- 2. chord map 0x02 --------*/
|
||||||
|
/* F0 60 02 04 <21B pitch> <21B chord> F7 : write whole map; reply in 02 01 format */
|
||||||
|
static void handleResetChordMap(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t key;
|
||||||
|
for (key = 1; key <= 21; key++)
|
||||||
|
{
|
||||||
|
BT_Pitch_offset_map(key, data[3 + key]); /* data[4..24] pitch offsets */
|
||||||
|
BT_Chord_offset_map(key, data[24 + key]); /* data[25..45] chord types */
|
||||||
|
}
|
||||||
|
handleReadRhythmMap(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*-------- 3. rhythm style 0x03 0x04 --------*/
|
||||||
|
static void handleRhythmStyle(uint8_t *data)
|
||||||
|
{
|
||||||
|
switch (data[4])
|
||||||
|
{
|
||||||
|
case 0: /* read current style: F0 60 03 04 00 <src> <idHi> <idLo> F7 */
|
||||||
|
{
|
||||||
|
uint8_t resp[9] = {0xF0, 0x60, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0xF7};
|
||||||
|
uint8_t src = (uint8_t)(mGuiData[GUI_AUTOBAND_SW].Current ? 1 : 0);
|
||||||
|
uint16_t id = src ? ParamGuiData[EXPRESS_MODE_PARAM].Current
|
||||||
|
: ParamGuiData[SONG_MODE_PARAM].Current;
|
||||||
|
resp[5] = src;
|
||||||
|
resp[6] = (uint8_t)(id / 128);
|
||||||
|
resp[7] = (uint8_t)(id % 128);
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 1: /* set style: F0 60 03 04 01 <src:0 sys/1 user> <idHi> <idLo> F7, reply BPM */
|
||||||
|
{
|
||||||
|
uint8_t src = (data[5] != 0) ? 1 : 0;
|
||||||
|
uint16_t id = (uint16_t)data[6] * 128 + data[7];
|
||||||
|
GUI_SWITCH *slot = src ? &ParamGuiData[EXPRESS_MODE_PARAM]
|
||||||
|
: &ParamGuiData[SONG_MODE_PARAM];
|
||||||
|
uint8_t resp[7] = {0xF0, 0x60, 0x03, 0x04, 0x01, 0x00, 0xF7};
|
||||||
|
if (src)
|
||||||
|
slot->Max = (LOCAL_SONG_COUNT > 0) ? (uint16_t)(LOCAL_SONG_COUNT - 1) : 0;
|
||||||
|
if (id > slot->Max)
|
||||||
|
id = slot->Max;
|
||||||
|
mGuiData[GUI_AUTOBAND_SW].Current = src;
|
||||||
|
slot->Current = id;
|
||||||
|
StartFlag = 0;
|
||||||
|
AutoBandTop1_Stop();
|
||||||
|
UI_ReloadTonePreset();
|
||||||
|
/* doc: single-byte bpm; clamp >127 (App should read exact bpm via 03 06) */
|
||||||
|
resp[5] = (mGuiData[GUI_SPEED].Current > 0x7F)
|
||||||
|
? 0x7F : (uint8_t)mGuiData[GUI_SPEED].Current;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
|
||||||
|
/* boot / song-entry special: F0 60 03 04 01 00 00 00 F7
|
||||||
|
doc: also push current BPM as active report F0 51 03 <hi> <lo> F7 */
|
||||||
|
if (data[5] == 0 && data[6] == 0 && data[7] == 0)
|
||||||
|
{
|
||||||
|
uint8_t bpmr[6] = {0xF0, 0x51, 0x03, 0x00, 0x00, 0xF7};
|
||||||
|
bpmr[3] = (uint8_t)(mGuiData[GUI_SPEED].Current / 128);
|
||||||
|
bpmr[4] = (uint8_t)(mGuiData[GUI_SPEED].Current % 128);
|
||||||
|
USART4_SendData(bpmr, sizeof(bpmr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2: /* user style list page: F0 60 03 04 02 <from3B> <to3B> F7
|
||||||
|
reply: F0 60 03 04 02 <cntHi> <cntLo> [<idHi> <idLo> <code 4B ascii> ...] F7 */
|
||||||
|
{
|
||||||
|
uint8_t resp[64];
|
||||||
|
uint32_t from = 0, to = LOCAL_SONG_COUNT;
|
||||||
|
uint16_t i, n = 0, len;
|
||||||
|
/* from/to: 24-bit big-endian, optional (doc example: 00 00 00 07) */
|
||||||
|
if (data[5] || data[6] || data[7])
|
||||||
|
{
|
||||||
|
from = ((uint32_t)data[5] << 16) | ((uint32_t)data[6] << 8) | data[7];
|
||||||
|
to = ((uint32_t)data[8] << 16) | ((uint32_t)data[9] << 8) | data[10];
|
||||||
|
}
|
||||||
|
if (from > LOCAL_SONG_COUNT) from = LOCAL_SONG_COUNT;
|
||||||
|
if (to > LOCAL_SONG_COUNT || to <= from) to = LOCAL_SONG_COUNT;
|
||||||
|
resp[0] = 0xF0; resp[1] = 0x60; resp[2] = 0x03; resp[3] = 0x04; resp[4] = 0x02;
|
||||||
|
len = 7; /* count filled after loop */
|
||||||
|
for (i = (uint16_t)from; i < to; i++)
|
||||||
|
{
|
||||||
|
if (len + 6 + 1 > sizeof(resp)) break;
|
||||||
|
resp[len++] = (uint8_t)(i / 128);
|
||||||
|
resp[len++] = (uint8_t)(i % 128);
|
||||||
|
memcpy(&resp[len], LocalSongCode[i], 4);
|
||||||
|
len += 4;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
resp[5] = (uint8_t)(n / 128);
|
||||||
|
resp[6] = (uint8_t)(n % 128);
|
||||||
|
resp[len++] = 0xF7;
|
||||||
|
USART4_SendData(resp, len);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3: /* user style total: reply F0 60 03 04 00 <total 3B BE> F7 (doc: 00 00 1E) */
|
||||||
|
{
|
||||||
|
uint8_t resp[9] = {0xF0, 0x60, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0xF7};
|
||||||
|
resp[5] = (uint8_t)((LOCAL_SONG_COUNT >> 16) & 0xFF);
|
||||||
|
resp[6] = (uint8_t)((LOCAL_SONG_COUNT >> 8) & 0xFF);
|
||||||
|
resp[7] = (uint8_t)(LOCAL_SONG_COUNT & 0xFF);
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*-------- 5. reset / power off 0x05 --------*/
|
||||||
|
/* F0 60 05 00 F7 : ack then soft power off (executed in main-loop context) */
|
||||||
|
static void handleDeviceReset(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t resp[6] = {0xF0, 0x60, 0x05, 0x00, 0x01, 0xF7};
|
||||||
|
(void)data;
|
||||||
|
LOG_I("BLE", "05 00 ack then soft power off");
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/* Auto-generated by tools/gen_git_user_fw_ver.py — do not edit.
|
||||||
|
* git HEAD (dirty): d4c35b8b909e2c4e5a520a347ab2c3edc85305f4
|
||||||
|
* branch: feature_bank → feature-bank
|
||||||
|
* 01 0C wire: feature-bank_d4c35b*
|
||||||
|
*/
|
||||||
|
#ifndef GIT_USER_FW_VER_H
|
||||||
|
#define GIT_USER_FW_VER_H
|
||||||
|
|
||||||
|
#define GIT_COMMIT_ID_FULL "d4c35b8b909e2c4e5a520a347ab2c3edc85305f4"
|
||||||
|
#define GIT_BRANCH_NAME "feature-bank"
|
||||||
|
#define GIT_COMMIT_SHORT6 "d4c35b"
|
||||||
|
#define GIT_DIRTY (1)
|
||||||
|
#define GIT_BUILD_ID "feature-bank_d4c35b*"
|
||||||
|
#define GIT_BUILD_ID_LEN 20u
|
||||||
|
|
||||||
|
#endif /* GIT_USER_FW_VER_H */
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
@echo off
|
||||||
|
REM Launch K1 HIL harness from repo root or any cwd.
|
||||||
|
REM Usage: run_k1_harness.bat selftest
|
||||||
|
REM run_k1_harness.bat run --suite all --profile nightly
|
||||||
|
setlocal
|
||||||
|
cd /d "%~dp0tools"
|
||||||
|
python -m k1_harness %*
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Repo-root launcher: python run_k1_harness.py ... (same as cd tools && python -m k1_harness ...)"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
TOOLS = os.path.join(ROOT, "tools")
|
||||||
|
if TOOLS not in sys.path:
|
||||||
|
sys.path.insert(0, TOOLS)
|
||||||
|
os.chdir(TOOLS)
|
||||||
|
|
||||||
|
from k1_harness.cli import main # noqa: E402
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -13,7 +13,7 @@ ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskUIThread_Stack[2048];
|
||||||
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskTouchThread_Stack[1536];
|
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskTouchThread_Stack[1536];
|
||||||
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskMainThread_Stack[512];
|
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskMainThread_Stack[512];
|
||||||
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskBTTHandlehread_Stack[512];
|
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskBTTHandlehread_Stack[512];
|
||||||
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskBTRecvThread_Stack[512];
|
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskBTRecvThread_Stack[768];
|
||||||
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskAutobandThread_Stack[512];
|
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskAutobandThread_Stack[512];
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -115,6 +115,9 @@ void TaskBTHandleThread_entry(void* parameter)
|
||||||
case MSG_ID_LIGHT_LED : BL_Set_led(xEvent.ID,xEvent.HiByte); break;
|
case MSG_ID_LIGHT_LED : BL_Set_led(xEvent.ID,xEvent.HiByte); break;
|
||||||
case MSG_ID_PITCH_OFFSET : BT_Pitch_offset_map(xEvent.ID,xEvent.HiByte); break;
|
case MSG_ID_PITCH_OFFSET : BT_Pitch_offset_map(xEvent.ID,xEvent.HiByte); break;
|
||||||
case MSG_ID_CHORD_OFFSET : BT_Chord_offset_map(xEvent.ID,xEvent.HiByte); break;
|
case MSG_ID_CHORD_OFFSET : BT_Chord_offset_map(xEvent.ID,xEvent.HiByte); break;
|
||||||
|
/* BLE section / intro-outro-end commands (06 xx, 04 07) */
|
||||||
|
case MSG_ID_ADCIN1KEY : ADC_IN1_KEY_Handle((uint8_t)xEvent.ID, true); break;
|
||||||
|
case MSG_ID_KEY_1617 : TM1617_Handle((uint8_t)xEvent.ID); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -125,11 +128,10 @@ void TaskBTRecvThread_entry(void* parameter)
|
||||||
uint8_t c;
|
uint8_t c;
|
||||||
while(1)
|
while(1)
|
||||||
{
|
{
|
||||||
if(rt_sem_take(UART4_sem,RT_WAITING_FOREVER) == RT_EOK)
|
if(rt_sem_take(UART4_sem, RT_WAITING_FOREVER) == RT_EOK)
|
||||||
{
|
{
|
||||||
while(UART4_RxPop(&c))
|
while(UART4_RxPop(&c))
|
||||||
{
|
{
|
||||||
//解析函数
|
|
||||||
UART4_Data_Process(&c);
|
UART4_Data_Process(&c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -238,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,
|
||||||
|
|
@ -334,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);
|
||||||
|
|
@ -354,6 +379,10 @@ 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Systematic BLE channel matrix for Smart Guitar MIDI App SysEx."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from bleak import BleakClient, BleakScanner
|
||||||
|
|
||||||
|
NAME = "Smart Guitar MIDI"
|
||||||
|
MIDI = "7772e5db-3868-4112-a1a9-f2669d106bf3"
|
||||||
|
UARTW = "e49a25e0-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||||
|
UARTN = "e49a28e1-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||||
|
EFF2 = "0000eff2-0000-1000-8000-00805f9b34fb"
|
||||||
|
|
||||||
|
SYSEX = bytes.fromhex("F0600101F7")
|
||||||
|
FRAMED = bytes.fromhex("8080F0600101F7")
|
||||||
|
FRAMED_TSF7 = bytes.fromhex("8080F060010180F7")
|
||||||
|
|
||||||
|
|
||||||
|
def hx(b: bytes) -> str:
|
||||||
|
return " ".join(f"{x:02X}" for x in b) if b else "(none)"
|
||||||
|
|
||||||
|
|
||||||
|
async def find(timeout=60.0):
|
||||||
|
t0 = time.time()
|
||||||
|
while time.time() - t0 < timeout:
|
||||||
|
d = await BleakScanner.find_device_by_filter(
|
||||||
|
lambda d, a: d.name and NAME.lower() in d.name.lower(), timeout=8
|
||||||
|
)
|
||||||
|
if d:
|
||||||
|
return d
|
||||||
|
print("waiting advertise...")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def unpair_if_needed(addr: str):
|
||||||
|
try:
|
||||||
|
c = BleakClient(addr, timeout=15)
|
||||||
|
await c.connect()
|
||||||
|
try:
|
||||||
|
await c.unpair()
|
||||||
|
print("unpaired")
|
||||||
|
except Exception as e:
|
||||||
|
print("unpair skip:", e)
|
||||||
|
try:
|
||||||
|
await c.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
except Exception as e:
|
||||||
|
print("unpair session:", e)
|
||||||
|
|
||||||
|
|
||||||
|
async def one_session(tag: str, write_uuid: str, payload: bytes, notify_uuids, settle=2.5):
|
||||||
|
d = await find(45)
|
||||||
|
if not d:
|
||||||
|
print(f"[{tag}] NO DEVICE")
|
||||||
|
return {"tag": tag, "ok": False, "err": "no device", "notifs": []}
|
||||||
|
print(f"\n=== {tag} === write {write_uuid[:8]} {hx(payload)}")
|
||||||
|
notifs = []
|
||||||
|
err = None
|
||||||
|
connected_end = False
|
||||||
|
try:
|
||||||
|
async with BleakClient(d, timeout=25) as c:
|
||||||
|
print("connected", c.is_connected)
|
||||||
|
|
||||||
|
def cb(sender, data):
|
||||||
|
b = bytes(data)
|
||||||
|
notifs.append(b)
|
||||||
|
print(f" NOTIFY {hx(b)}")
|
||||||
|
|
||||||
|
for u in notify_uuids:
|
||||||
|
try:
|
||||||
|
await c.start_notify(u, cb)
|
||||||
|
print(" notify on", u[:8])
|
||||||
|
except Exception as e:
|
||||||
|
print(" notify fail", u[:8], e)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
try:
|
||||||
|
await c.write_gatt_char(write_uuid, payload, response=False)
|
||||||
|
print(" write ok")
|
||||||
|
except Exception as e:
|
||||||
|
err = f"write: {e}"
|
||||||
|
print(" write fail", e)
|
||||||
|
await asyncio.sleep(settle)
|
||||||
|
connected_end = c.is_connected
|
||||||
|
print(" end conn=", connected_end, "notifs=", len(notifs))
|
||||||
|
except Exception as e:
|
||||||
|
err = str(e)
|
||||||
|
print(" session err", e)
|
||||||
|
return {
|
||||||
|
"tag": tag,
|
||||||
|
"ok": any(b[:3] == b"\xF0\x60\x01" or b[:4] == b"\x80\x80\xF0\x60" for b in notifs),
|
||||||
|
"notifs": [hx(b) for b in notifs],
|
||||||
|
"conn_end": connected_end,
|
||||||
|
"err": err,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
d = await find(60)
|
||||||
|
if not d:
|
||||||
|
print("FATAL: device not advertising")
|
||||||
|
return
|
||||||
|
print("found", d.name, d.address)
|
||||||
|
await unpair_if_needed(d.address)
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
# matrix: do NOT put uartw first if it kills radio; do midi first
|
||||||
|
cases = [
|
||||||
|
("midi-framed", MIDI, FRAMED, [MIDI, UARTN, EFF2]),
|
||||||
|
("midi-framed-tsF7", MIDI, FRAMED_TSF7, [MIDI]),
|
||||||
|
("midi-raw", MIDI, SYSEX, [MIDI]),
|
||||||
|
("eff2-raw", EFF2, SYSEX, [EFF2, MIDI, UARTN]),
|
||||||
|
("eff2-framed", EFF2, FRAMED, [EFF2, MIDI]),
|
||||||
|
("uartw-raw", UARTW, SYSEX, [UARTN, MIDI, EFF2]),
|
||||||
|
("uartw-framed", UARTW, FRAMED, [UARTN, MIDI]),
|
||||||
|
]
|
||||||
|
for tag, wu, payload, nus in cases:
|
||||||
|
r = await one_session(tag, wu, payload, nus)
|
||||||
|
results.append(r)
|
||||||
|
# if uart killed advertising, wait/reset hint
|
||||||
|
if not r.get("conn_end", True):
|
||||||
|
print("link dropped; waiting re-advertise...")
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
if not await find(30):
|
||||||
|
print("device gone after drop — stop matrix (need JLink reset)")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
print("\n===== MATRIX SUMMARY =====")
|
||||||
|
for r in results:
|
||||||
|
status = "PASS" if r["ok"] else "FAIL"
|
||||||
|
print(f"{status:4} {r['tag']:18} conn_end={r.get('conn_end')} notifs={r['notifs'] or ['(none)']} err={r.get('err')}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Probe: RTT log + BLE uartw write to see if MCU gets F0 60."""
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import pylink
|
||||||
|
from bleak import BleakScanner, BleakClient
|
||||||
|
|
||||||
|
UARTW = "e49a25e0-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||||
|
UARTN = "e49a28e1-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||||
|
MIDI = "7772e5db-3868-4112-a1a9-f2669d106bf3"
|
||||||
|
|
||||||
|
|
||||||
|
def open_jlink():
|
||||||
|
j = pylink.JLink()
|
||||||
|
j.open()
|
||||||
|
try:
|
||||||
|
j.exec_command("HideDeviceSelection = 1")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
j.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
||||||
|
last = None
|
||||||
|
for dev in ("Cortex-M4", "AT32F403AC", "AT32F403A"):
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
j.exec_command(f"Device = {dev}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
j.connect(dev)
|
||||||
|
print("JLink connected", dev, flush=True)
|
||||||
|
try:
|
||||||
|
j.restart(halt=False)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
j.go()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return j
|
||||||
|
except Exception as e:
|
||||||
|
last = e
|
||||||
|
raise SystemExit(f"JLink connect failed: {last}")
|
||||||
|
|
||||||
|
|
||||||
|
async def find_dev(timeout=60):
|
||||||
|
t0 = time.time()
|
||||||
|
while time.time() - t0 < timeout:
|
||||||
|
d = await BleakScanner.find_device_by_filter(
|
||||||
|
lambda d, a: d.name and "Smart Guitar" in d.name, timeout=8
|
||||||
|
)
|
||||||
|
if d:
|
||||||
|
return d
|
||||||
|
print("waiting BLE...", flush=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
j = open_jlink()
|
||||||
|
j.rtt_start()
|
||||||
|
stop = False
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
def reader():
|
||||||
|
while not stop:
|
||||||
|
try:
|
||||||
|
data = j.rtt_read(0, 4096)
|
||||||
|
if data:
|
||||||
|
s = bytes(data).decode("utf-8", "replace")
|
||||||
|
lines.append(s)
|
||||||
|
print("RTT>", s, end="" if s.endswith("\n") else "\n", flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
print("rtt err", e, flush=True)
|
||||||
|
break
|
||||||
|
time.sleep(0.03)
|
||||||
|
|
||||||
|
th = threading.Thread(target=reader, daemon=True)
|
||||||
|
th.start()
|
||||||
|
print("collect boot logs 3s...", flush=True)
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
d = await find_dev(90)
|
||||||
|
if not d:
|
||||||
|
print("NO BLE DEVICE", flush=True)
|
||||||
|
stop = True
|
||||||
|
j.close()
|
||||||
|
return
|
||||||
|
print("BLE", d.name, d.address, flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with BleakClient(d, timeout=30) as c:
|
||||||
|
print("connected", flush=True)
|
||||||
|
notifs = []
|
||||||
|
|
||||||
|
def cb(_s, data):
|
||||||
|
notifs.append(bytes(data))
|
||||||
|
print("NOTIFY", data.hex(), flush=True)
|
||||||
|
|
||||||
|
for u in (UARTN, MIDI):
|
||||||
|
try:
|
||||||
|
await c.start_notify(u, cb)
|
||||||
|
print("notify ok", u[:8], flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
print("notify fail", u[:8], e, flush=True)
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
print("WRITE uartw F0 60 01 01 F7", flush=True)
|
||||||
|
try:
|
||||||
|
await c.write_gatt_char(UARTW, bytes.fromhex("F0600101F7"), response=False)
|
||||||
|
print("write returned", flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
print("write err", e, flush=True)
|
||||||
|
await asyncio.sleep(4)
|
||||||
|
print("notifs", len(notifs), "conn", c.is_connected, flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
print("BLE session err", e, flush=True)
|
||||||
|
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
stop = True
|
||||||
|
time.sleep(0.3)
|
||||||
|
j.close()
|
||||||
|
print("RTT total chunks", len(lines), flush=True)
|
||||||
|
ble_hits = [x for x in lines if "BLE" in x or "sysex" in x]
|
||||||
|
print("BLE-related RTT lines:", ble_hits, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Flash is separate; this attaches RTT then writes BLE SysEx and prints MCU logs."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pylink
|
||||||
|
from bleak import BleakClient, BleakScanner
|
||||||
|
|
||||||
|
NAME = "Smart Guitar MIDI"
|
||||||
|
MIDI = "7772e5db-3868-4112-a1a9-f2669d106bf3"
|
||||||
|
UARTW = "e49a25e0-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||||
|
UARTN = "e49a28e1-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||||
|
EFF2 = "0000eff2-0000-1000-8000-00805f9b34fb"
|
||||||
|
|
||||||
|
|
||||||
|
def open_jlink():
|
||||||
|
j = pylink.JLink()
|
||||||
|
j.open()
|
||||||
|
try:
|
||||||
|
j.exec_command("HideDeviceSelection = 1")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
j.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
||||||
|
last = None
|
||||||
|
for dev in ("Cortex-M4", "AT32F403AC", "AT32F403A"):
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
j.exec_command(f"Device = {dev}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
j.connect(dev)
|
||||||
|
print("JLink OK", dev, flush=True)
|
||||||
|
try:
|
||||||
|
j.restart(halt=False)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
j.go()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return j
|
||||||
|
except Exception as e:
|
||||||
|
last = e
|
||||||
|
raise SystemExit(f"JLink fail: {last}")
|
||||||
|
|
||||||
|
|
||||||
|
async def find_dev(timeout=45.0):
|
||||||
|
t0 = time.time()
|
||||||
|
while time.time() - t0 < timeout:
|
||||||
|
d = await BleakScanner.find_device_by_filter(
|
||||||
|
lambda d, a: d.name and NAME.lower() in d.name.lower(), timeout=8
|
||||||
|
)
|
||||||
|
if d:
|
||||||
|
return d
|
||||||
|
print("wait BLE...", flush=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def ble_write(tag, write_uuid, payload, notify_uuids):
|
||||||
|
d = await find_dev()
|
||||||
|
if not d:
|
||||||
|
print("NO BLE", flush=True)
|
||||||
|
return
|
||||||
|
print(f"BLE {d.address} write {tag} {payload.hex()}", flush=True)
|
||||||
|
try:
|
||||||
|
c = BleakClient(d.address, timeout=20)
|
||||||
|
await c.connect()
|
||||||
|
try:
|
||||||
|
await c.unpair()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await c.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
d = await find_dev() or d
|
||||||
|
notifs = []
|
||||||
|
async with BleakClient(d, timeout=25) as c:
|
||||||
|
def cb(_s, data):
|
||||||
|
notifs.append(bytes(data))
|
||||||
|
print("GATT NOTIFY", data.hex(), flush=True)
|
||||||
|
|
||||||
|
for u in notify_uuids:
|
||||||
|
try:
|
||||||
|
await c.start_notify(u, cb)
|
||||||
|
except Exception as e:
|
||||||
|
print("notify fail", u[:8], e, flush=True)
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
try:
|
||||||
|
await c.write_gatt_char(write_uuid, payload, response=False)
|
||||||
|
print("GATT write ok, conn", c.is_connected, flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
print("GATT write fail", e, flush=True)
|
||||||
|
await asyncio.sleep(3.0)
|
||||||
|
print("GATT notifs", len(notifs), "conn", c.is_connected, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
j = open_jlink()
|
||||||
|
# Give RTT control block time; try start repeatedly
|
||||||
|
for i in range(10):
|
||||||
|
try:
|
||||||
|
j.rtt_start(block_address=0x20016D68)
|
||||||
|
st = j.rtt_get_status()
|
||||||
|
print("RTT status", st, flush=True)
|
||||||
|
if getattr(st, "NumUpBuffers", 0):
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print("rtt_start", e, flush=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
else:
|
||||||
|
print("WARN: RTT upbuffers still 0 — will keep reading anyway", flush=True)
|
||||||
|
|
||||||
|
stop = False
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
def reader():
|
||||||
|
while not stop:
|
||||||
|
try:
|
||||||
|
data = j.rtt_read(0, 4096)
|
||||||
|
if data:
|
||||||
|
s = bytes(data).decode("utf-8", "replace")
|
||||||
|
lines.append(s)
|
||||||
|
print("RTT>", s, end="" if s.endswith("\n") else "\n", flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
print("rtt read err", e, flush=True)
|
||||||
|
break
|
||||||
|
time.sleep(0.03)
|
||||||
|
|
||||||
|
th = threading.Thread(target=reader, daemon=True)
|
||||||
|
th.start()
|
||||||
|
print("collect 4s boot/idle logs...", flush=True)
|
||||||
|
await asyncio.sleep(4.0)
|
||||||
|
if not lines:
|
||||||
|
print("WARNING: no RTT yet — touch screen or wait; continuing BLE test", flush=True)
|
||||||
|
|
||||||
|
# 1) BLE-MIDI framed (stable path)
|
||||||
|
await ble_write(
|
||||||
|
"midi-framed",
|
||||||
|
MIDI,
|
||||||
|
bytes.fromhex("8080F0600101F7"),
|
||||||
|
[MIDI, UARTN, EFF2],
|
||||||
|
)
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
|
||||||
|
# E49A raw write often drops link / stops advertising — skip by default
|
||||||
|
if "--with-uartw" in sys.argv:
|
||||||
|
await ble_write(
|
||||||
|
"uartw-raw",
|
||||||
|
UARTW,
|
||||||
|
bytes.fromhex("F0600101F7"),
|
||||||
|
[UARTN, MIDI, EFF2],
|
||||||
|
)
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
stop = True
|
||||||
|
time.sleep(0.4)
|
||||||
|
j.close()
|
||||||
|
|
||||||
|
text = "".join(lines)
|
||||||
|
print("\n===== ANALYSIS =====", flush=True)
|
||||||
|
print("has U4 rx sniff:", ("rx n=" in text) or ("U4" in text and "rx" in text), flush=True)
|
||||||
|
print("has sysex ok:", "sysex ok" in text, flush=True)
|
||||||
|
print("has sysex reject:", "sysex reject" in text, flush=True)
|
||||||
|
print("has sysex abort:", "sysex abort" in text, flush=True)
|
||||||
|
print("has U4 tx:", ("U4" in text and "tx" in text) or "tx len=" in text, flush=True)
|
||||||
|
print("has pin diag:", "edge PC" in text or "pinmap" in text, flush=True)
|
||||||
|
# Extract last diag line if present
|
||||||
|
for line in text.splitlines():
|
||||||
|
if "edge PC10=" in line:
|
||||||
|
print("diag:", line.strip(), flush=True)
|
||||||
|
if "sysex ok" in text and "tx len=" in text:
|
||||||
|
print("VERDICT: MCU got cmd and replied on UART4 → GATT notify path broken in BLE module", flush=True)
|
||||||
|
elif "rx n=" in text and "sysex ok" not in text:
|
||||||
|
print("VERDICT: UART4 got bytes but frame not accepted → protocol/framing", flush=True)
|
||||||
|
elif "edge PC12=" in text and "isr_rx=0" in text:
|
||||||
|
# SCH puts B_UART4_RX on PC12; FW UART4 RX is PC11
|
||||||
|
print("VERDICT: activity on PC12 while UART4 ISR idle → driver pinmap vs SCH (MCU底层)", flush=True)
|
||||||
|
elif "edge PC11=" in text and "isr_rx=0" in text:
|
||||||
|
print("VERDICT: edges on PC11 but no UART ISR → baud/noise or not UART framing", flush=True)
|
||||||
|
elif "isr_rx=0" in text and "edge PC10=0" in text and "edge PC11=0" in text and "edge PC12=0" in text:
|
||||||
|
print("VERDICT: no GPIO edges + no UART ISR → ATS2853 did not drive UART (模组固件/桥接)", flush=True)
|
||||||
|
elif "U4" not in text and "rx" not in text:
|
||||||
|
print("VERDICT: no UART4 activity → BLE module did not forward GATT write to UART4 (or RTT dead)", flush=True)
|
||||||
|
else:
|
||||||
|
print("VERDICT: inconclusive — see RTT dump above", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
"""One-off: expert rhythm (1.bin) piece mode ends after the piece window;
|
||||||
|
press fretboard only (no pick) after it stops -> must restart (trigger=fret + PITCH)."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import rtt_pitch_reg_test as R
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
R.import_deps()
|
||||||
|
jlink = R.connect_jlink("AT32F403AC") # same default as rtt_pitch_reg_test
|
||||||
|
try:
|
||||||
|
cb = R.find_rtt_control_block(jlink)
|
||||||
|
if cb is None:
|
||||||
|
print("FAIL: RTT CB not found")
|
||||||
|
return 1
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
R.wait_rtt_ready(jlink)
|
||||||
|
sess = R.RttSession(jlink)
|
||||||
|
sess.pump(0.3)
|
||||||
|
|
||||||
|
sess.cmd("log clear", 0.3)
|
||||||
|
sess.cmd("tone bin1 0", 1.2) # expert + rhythm 1.bin (piece 0..1920ms)
|
||||||
|
sess.cmd("tone start", 0.8) # pick start, piece plays then stops
|
||||||
|
print(">> playing window (piece ~2s) ...", flush=True)
|
||||||
|
sess.pump(4.0) # wait until piece ended, PlayTick frozen
|
||||||
|
|
||||||
|
base = len(sess.lines)
|
||||||
|
sess.cmd("chord key 0", 0.4)
|
||||||
|
sess.cmd("chord key 8", 0.2) # fretboard only, no pick
|
||||||
|
sess.pump(3.0) # capture restart
|
||||||
|
|
||||||
|
new = sess.lines[base:]
|
||||||
|
joined = "\n".join(new)
|
||||||
|
trig = [l for l in new if "trigger=fret" in l]
|
||||||
|
change = [l for l in new if "expert/fret change" in l]
|
||||||
|
pitch = [l for l in new if "[PITCH] on" in l]
|
||||||
|
print("---- full session ----", flush=True)
|
||||||
|
for l in sess.lines:
|
||||||
|
print(l, flush=True)
|
||||||
|
print("---- captured after fret-only press ----", flush=True)
|
||||||
|
for l in new:
|
||||||
|
print(l, flush=True)
|
||||||
|
print("---- verdict ----", flush=True)
|
||||||
|
handled = trig or change
|
||||||
|
if handled and pitch:
|
||||||
|
path = "restart" if trig else "note_on-change"
|
||||||
|
print(f"PASS fret-only continue: path={path}, PITCH on x{len(pitch)}")
|
||||||
|
return 0
|
||||||
|
print("FAIL: no handler log or no pitch")
|
||||||
|
return 1
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
sess.cmd("chord key 23", 0.3) # stop band, leave device quiet
|
||||||
|
sess.pump(0.3)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""RTT verify normal-mode: hold pad + pick sounds; release stops.
|
||||||
|
|
||||||
|
Uses real physical pad hold (inject is cleared by TM1629 scan within one tick).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, ".")
|
||||||
|
from rtt_pitch_reg_test import ( # noqa: E402
|
||||||
|
RttSession,
|
||||||
|
connect_jlink,
|
||||||
|
find_rtt_control_block,
|
||||||
|
wait_rtt_ready,
|
||||||
|
)
|
||||||
|
|
||||||
|
SPEED_TAIL = struct.pack("<HHH", 40, 260, 1)
|
||||||
|
GUI_SWITCH_SIZE = 8
|
||||||
|
TAB_INDEX = 5
|
||||||
|
|
||||||
|
|
||||||
|
def count_pitch(lines: list[str]) -> int:
|
||||||
|
return sum(1 for L in lines if "[PITCH]" in L and " on " in L)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tab(lines: list[str]) -> int | None:
|
||||||
|
for L in reversed(lines):
|
||||||
|
if "TONE tab=" not in L:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
return int(L.split("tab=")[1].split()[0])
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def with_halt(jlink, fn):
|
||||||
|
was = jlink.halted()
|
||||||
|
if not was:
|
||||||
|
jlink.halt()
|
||||||
|
try:
|
||||||
|
return fn()
|
||||||
|
finally:
|
||||||
|
if not was:
|
||||||
|
if hasattr(jlink, "restart"):
|
||||||
|
jlink.restart()
|
||||||
|
else:
|
||||||
|
jlink.go()
|
||||||
|
time.sleep(0.02)
|
||||||
|
|
||||||
|
|
||||||
|
def find_pattern(jlink, needle: bytes, ram_base=0x20000000, ram_size=0x18000) -> int | None:
|
||||||
|
def _find():
|
||||||
|
chunk = 0x1000
|
||||||
|
for off in range(0, ram_size, chunk):
|
||||||
|
data = bytes(jlink.memory_read8(ram_base + off, min(chunk, ram_size - off)))
|
||||||
|
idx = data.find(needle)
|
||||||
|
if idx >= 0:
|
||||||
|
return ram_base + off + idx
|
||||||
|
return None
|
||||||
|
|
||||||
|
return with_halt(jlink, _find)
|
||||||
|
|
||||||
|
|
||||||
|
def mem_write(jlink, addr: int, data: bytes) -> None:
|
||||||
|
with_halt(jlink, lambda: jlink.memory_write8(addr, list(data)))
|
||||||
|
|
||||||
|
|
||||||
|
def mem_read(jlink, addr: int, n: int) -> bytes:
|
||||||
|
return with_halt(jlink, lambda: bytes(jlink.memory_read8(addr, n)))
|
||||||
|
|
||||||
|
|
||||||
|
def force_tab_normal(jlink) -> bool:
|
||||||
|
hit = find_pattern(jlink, SPEED_TAIL)
|
||||||
|
if hit is None:
|
||||||
|
print("!! GUI_SPEED row not found", flush=True)
|
||||||
|
return False
|
||||||
|
base = (hit - 2) - GUI_SWITCH_SIZE
|
||||||
|
tab_addr = base + TAB_INDEX * GUI_SWITCH_SIZE
|
||||||
|
mem_write(jlink, tab_addr, struct.pack("<H", 2))
|
||||||
|
got = struct.unpack("<H", mem_read(jlink, tab_addr, 2))[0]
|
||||||
|
print(f"mGuiData @ 0x{base:08X} forced TAB={got}", flush=True)
|
||||||
|
return got == 2
|
||||||
|
|
||||||
|
|
||||||
|
def wait_enter(msg: str, seconds: float) -> None:
|
||||||
|
print(f"\n>>> {msg}", flush=True)
|
||||||
|
print(f" ({seconds:.0f}s countdown)", flush=True)
|
||||||
|
for left in range(int(seconds), 0, -1):
|
||||||
|
print(f" {left}...", flush=True)
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
j = connect_jlink("Cortex-M4")
|
||||||
|
cb = find_rtt_control_block(j)
|
||||||
|
if not cb:
|
||||||
|
raise SystemExit("RTT CB not found")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||||||
|
j.rtt_start(cb)
|
||||||
|
wait_rtt_ready(j)
|
||||||
|
sess = RttSession(j)
|
||||||
|
|
||||||
|
print("\n=== Setup: 1.bin + TAB=普通(2) ===", flush=True)
|
||||||
|
sess.cmd("log clear", 0.2)
|
||||||
|
sess.cmd_ack("tone bin1 0", "TONE_BIN1", timeout_s=4.0, retries=4)
|
||||||
|
if not force_tab_normal(j):
|
||||||
|
j.rtt_stop()
|
||||||
|
j.close()
|
||||||
|
return 3
|
||||||
|
sess.cmd_ack("tone status", "TONE tab=", timeout_s=3.0, retries=4)
|
||||||
|
print(f"tab={parse_tab(sess.lines)}", flush=True)
|
||||||
|
|
||||||
|
# Stop any leftover
|
||||||
|
sess.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
|
||||||
|
force_tab_normal(j)
|
||||||
|
|
||||||
|
print("\n=== TEST1: pick with NO pad hold (expect silence) ===", flush=True)
|
||||||
|
print("请双手离开指板。", flush=True)
|
||||||
|
wait_enter("双手离开指板后等待自动拨片测试", 3)
|
||||||
|
force_tab_normal(j)
|
||||||
|
sess.cmd("log clear", 0.2)
|
||||||
|
sess.cmd_ack("tone start", "TONE_START_DONE", timeout_s=3.0, retries=5)
|
||||||
|
n1 = count_pitch(sess.pump(1.5))
|
||||||
|
print(f"TEST1 PITCH on={n1} (expect 0)", flush=True)
|
||||||
|
|
||||||
|
print("\n=== TEST2: hold pad + pick (expect sound) ===", flush=True)
|
||||||
|
wait_enter("请按住指板任意和弦键不放(建议按住第2排中部)", 5)
|
||||||
|
force_tab_normal(j)
|
||||||
|
sess.cmd("log clear", 0.2)
|
||||||
|
# keep KEY from physical hold; tone start without clearing if we use 'k'
|
||||||
|
# But we don't know KEY — physical hold already set KEY_ID+PressFlag via scan.
|
||||||
|
# tone start WITHOUT k clears KEY_ID! Must use tone start k — but then KEY kept.
|
||||||
|
# If user is holding, KEY_ID is set; PressFlag is set. tone start sets StartFlag=0
|
||||||
|
# then clears KEY unless k. Use: don't clear — need tone start k AND physical KEY already set.
|
||||||
|
sess.cmd_ack("tone start k", "TONE_START_DONE", timeout_s=3.0, retries=5)
|
||||||
|
play = sess.pump(2.5)
|
||||||
|
n2 = count_pitch(play)
|
||||||
|
print(f"TEST2 PITCH on={n2} (expect >0) — keep holding!", flush=True)
|
||||||
|
for L in play:
|
||||||
|
if "[PITCH]" in L:
|
||||||
|
print(" ", L, flush=True)
|
||||||
|
break
|
||||||
|
|
||||||
|
print("\n=== TEST3: release pad (expect stop) ===", flush=True)
|
||||||
|
wait_enter("请松开指板(松手)", 3)
|
||||||
|
force_tab_normal(j)
|
||||||
|
sess.cmd("log clear", 0.15)
|
||||||
|
post = sess.pump(2.0)
|
||||||
|
n3 = count_pitch(post)
|
||||||
|
print(f"TEST3 PITCH on after release={n3} (expect 0)", flush=True)
|
||||||
|
|
||||||
|
print("\n=== TEST4: hold again + pick (expect sound) ===", flush=True)
|
||||||
|
wait_enter("请再次按住指板和弦键不放", 5)
|
||||||
|
force_tab_normal(j)
|
||||||
|
sess.cmd("log clear", 0.2)
|
||||||
|
sess.cmd_ack("tone start k", "TONE_START_DONE", timeout_s=3.0, retries=5)
|
||||||
|
n4 = count_pitch(sess.pump(2.0))
|
||||||
|
print(f"TEST4 PITCH on={n4} (expect >0)", flush=True)
|
||||||
|
print("可松开指板。", flush=True)
|
||||||
|
time.sleep(1.0)
|
||||||
|
sess.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
|
||||||
|
|
||||||
|
t1, t2, t3, t4 = (n1 == 0), (n2 >= 1), (n3 == 0), (n4 >= 1)
|
||||||
|
print("\n======== RESULT ========", flush=True)
|
||||||
|
print(f"TEST1 no-hold silence: {'PASS' if t1 else 'FAIL'} (n={n1})", flush=True)
|
||||||
|
print(f"TEST2 hold+pick sound: {'PASS' if t2 else 'FAIL'} (n={n2})", flush=True)
|
||||||
|
print(f"TEST3 release stop: {'PASS' if t3 else 'FAIL'} (n={n3})", flush=True)
|
||||||
|
print(f"TEST4 re-trigger: {'PASS' if t4 else 'FAIL'} (n={n4})", flush=True)
|
||||||
|
ok_all = t1 and t2 and t3 and t4
|
||||||
|
print("OVERALL:", "PASS" if ok_all else "FAIL", flush=True)
|
||||||
|
j.rtt_stop()
|
||||||
|
j.close()
|
||||||
|
return 0 if ok_all else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Quick USB MIDI (SAM5704) SysEx probe for App F0 60 frames."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import ctypes.wintypes as w
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
winmm = ctypes.WinDLL("winmm")
|
||||||
|
CALLBACK_FUNCTION = 0x30000
|
||||||
|
MIM_DATA, MIM_LONGDATA = 0x3C3, 0x3C4
|
||||||
|
MHDR_DONE = 0x01
|
||||||
|
|
||||||
|
|
||||||
|
class MIDIINCAPS(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("wMid", w.WORD), ("wPid", w.WORD), ("vDriverVersion", w.DWORD),
|
||||||
|
("szPname", ctypes.c_char * 32), ("dwSupport", w.DWORD),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MIDIOUTCAPS(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("wMid", w.WORD), ("wPid", w.WORD), ("vDriverVersion", w.DWORD),
|
||||||
|
("szPname", ctypes.c_char * 32), ("wTechnology", w.WORD),
|
||||||
|
("wVoices", w.WORD), ("wNotes", w.WORD), ("wChannelMask", w.WORD),
|
||||||
|
("dwSupport", w.DWORD),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MIDIHDR(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("lpData", ctypes.c_void_p),
|
||||||
|
("dwBufferLength", w.DWORD),
|
||||||
|
("dwBytesRecorded", w.DWORD),
|
||||||
|
("dwUser", ctypes.c_void_p),
|
||||||
|
("dwFlags", w.DWORD),
|
||||||
|
("lpNext", ctypes.c_void_p),
|
||||||
|
("reserved", ctypes.c_void_p),
|
||||||
|
("dwOffset", w.DWORD),
|
||||||
|
("dwReserved", ctypes.c_size_t * 8),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def find_sam(kind: str):
|
||||||
|
n = winmm.midiInGetNumDevs() if kind == "in" else winmm.midiOutGetNumDevs()
|
||||||
|
for i in range(n):
|
||||||
|
if kind == "in":
|
||||||
|
c = MIDIINCAPS()
|
||||||
|
winmm.midiInGetDevCapsA(i, ctypes.byref(c), ctypes.sizeof(c))
|
||||||
|
else:
|
||||||
|
c = MIDIOUTCAPS()
|
||||||
|
winmm.midiOutGetDevCapsA(i, ctypes.byref(c), ctypes.sizeof(c))
|
||||||
|
name = c.szPname.decode("mbcs", "replace")
|
||||||
|
if "SAM5704" in name:
|
||||||
|
return i, name
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def hx(b: bytes) -> str:
|
||||||
|
return " ".join(f"{x:02X}" for x in b)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
in_i, in_n = find_sam("in")
|
||||||
|
out_i, out_n = find_sam("out")
|
||||||
|
print(f"IN={in_i} {in_n} OUT={out_i} {out_n}", flush=True)
|
||||||
|
if in_i is None or out_i is None:
|
||||||
|
print("SAM5704 not found")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
received: list[bytes] = []
|
||||||
|
keep = [] # keep buffer refs alive
|
||||||
|
|
||||||
|
MidiInProc = ctypes.WINFUNCTYPE(
|
||||||
|
None, w.HANDLE, w.UINT, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_size_t
|
||||||
|
)
|
||||||
|
|
||||||
|
@MidiInProc
|
||||||
|
def on_midi_in(h, msg, _inst, p1, p2):
|
||||||
|
if msg == MIM_LONGDATA:
|
||||||
|
hdr = ctypes.cast(p1, ctypes.POINTER(MIDIHDR)).contents
|
||||||
|
n = hdr.dwBytesRecorded
|
||||||
|
if n and hdr.lpData:
|
||||||
|
data = ctypes.string_at(hdr.lpData, n)
|
||||||
|
received.append(data)
|
||||||
|
print("RX", hx(data), flush=True)
|
||||||
|
winmm.midiInAddBuffer(h, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||||
|
elif msg == MIM_DATA:
|
||||||
|
print(
|
||||||
|
f"RX SHORT {(p1 & 0xFF):02X} {((p1 >> 8) & 0xFF):02X} {((p1 >> 16) & 0xFF):02X}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
h_in = w.HANDLE()
|
||||||
|
r = winmm.midiInOpen(ctypes.byref(h_in), in_i, on_midi_in, 0, CALLBACK_FUNCTION)
|
||||||
|
print("midiInOpen", r, flush=True)
|
||||||
|
if r != 0:
|
||||||
|
return r
|
||||||
|
|
||||||
|
for _ in range(4):
|
||||||
|
buf = ctypes.create_string_buffer(1024)
|
||||||
|
hdr = MIDIHDR()
|
||||||
|
hdr.lpData = ctypes.cast(buf, ctypes.c_void_p)
|
||||||
|
hdr.dwBufferLength = 1024
|
||||||
|
keep.append((buf, hdr))
|
||||||
|
winmm.midiInPrepareHeader(h_in, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||||
|
winmm.midiInAddBuffer(h_in, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||||
|
winmm.midiInStart(h_in)
|
||||||
|
|
||||||
|
h_out = w.HANDLE()
|
||||||
|
r = winmm.midiOutOpen(ctypes.byref(h_out), out_i, 0, 0, 0)
|
||||||
|
print("midiOutOpen", r, flush=True)
|
||||||
|
if r != 0:
|
||||||
|
return r
|
||||||
|
|
||||||
|
def send_sysex(msg: bytes):
|
||||||
|
buf = ctypes.create_string_buffer(msg)
|
||||||
|
hdr = MIDIHDR()
|
||||||
|
hdr.lpData = ctypes.cast(buf, ctypes.c_void_p)
|
||||||
|
hdr.dwBufferLength = len(msg)
|
||||||
|
keep.append((buf, hdr))
|
||||||
|
winmm.midiOutPrepareHeader(h_out, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||||
|
print("TX", hx(msg), flush=True)
|
||||||
|
winmm.midiOutLongMsg(h_out, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||||
|
t0 = time.time()
|
||||||
|
while not (hdr.dwFlags & MHDR_DONE) and time.time() - t0 < 2:
|
||||||
|
time.sleep(0.01)
|
||||||
|
winmm.midiOutUnprepareHeader(h_out, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||||
|
|
||||||
|
for label, msg in [
|
||||||
|
("01 01", bytes.fromhex("F0600101F7")),
|
||||||
|
("01 02", bytes.fromhex("F0600102F7")),
|
||||||
|
("01 03", bytes.fromhex("F0600103F7")),
|
||||||
|
]:
|
||||||
|
print("---", label, flush=True)
|
||||||
|
n0 = len(received)
|
||||||
|
send_sysex(msg)
|
||||||
|
time.sleep(1.2)
|
||||||
|
print(" new rx", len(received) - n0, flush=True)
|
||||||
|
|
||||||
|
print("TOTAL RX", len(received), flush=True)
|
||||||
|
app_like = [x for x in received if len(x) >= 2 and x[0] == 0xF0 and x[1] == 0x60]
|
||||||
|
print("F0 60 replies", len(app_like), flush=True)
|
||||||
|
|
||||||
|
winmm.midiInStop(h_in)
|
||||||
|
winmm.midiInReset(h_in)
|
||||||
|
for buf, hdr in keep[:4]:
|
||||||
|
winmm.midiInUnprepareHeader(h_in, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||||
|
winmm.midiInClose(h_in)
|
||||||
|
winmm.midiOutClose(h_out)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
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.
|
|
@ -0,0 +1,343 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
ble_log_pull.py — 通过 BLE 拉取 K1 设备现场日志分区 (LOG.BIN)
|
||||||
|
|
||||||
|
协议 (SysEx, App->设备 F0 60 ... F7):
|
||||||
|
07 00 查询 meta
|
||||||
|
07 01 按 offset 读取分片 (每片最多 20 原始字节, nibble 编码)
|
||||||
|
07 02 清空 (需确认码 55 2A)
|
||||||
|
|
||||||
|
注意: 本模组上 UART GATT 裸写容易导致断连;默认优先 BLE-MIDI。
|
||||||
|
|
||||||
|
依赖: bleak
|
||||||
|
用法:
|
||||||
|
python ble_log_pull.py --out LOG.BIN --decode crash.log -v
|
||||||
|
python ble_log_pull.py --transport midi --address CB:4E:FD:F1:C0:79 --out LOG.BIN
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from test_protocol_ble import BleMidiSim, DEFAULT_NAME # noqa: E402
|
||||||
|
from test_protocol_app_sim import HEAD, APP_ID, build, hexs # noqa: E402
|
||||||
|
|
||||||
|
CHUNK = 20
|
||||||
|
|
||||||
|
|
||||||
|
def u32_to_7bit(v: int) -> bytes:
|
||||||
|
v &= 0xFFFFFFFF
|
||||||
|
return bytes(
|
||||||
|
[
|
||||||
|
(v >> 28) & 0x7F,
|
||||||
|
(v >> 21) & 0x7F,
|
||||||
|
(v >> 14) & 0x7F,
|
||||||
|
(v >> 7) & 0x7F,
|
||||||
|
v & 0x7F,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def u32_from_7bit(b: bytes, i: int = 0) -> int:
|
||||||
|
return (
|
||||||
|
((b[i] & 0x7F) << 28)
|
||||||
|
| ((b[i + 1] & 0x7F) << 21)
|
||||||
|
| ((b[i + 2] & 0x7F) << 14)
|
||||||
|
| ((b[i + 3] & 0x7F) << 7)
|
||||||
|
| (b[i + 4] & 0x7F)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def nibbles_to_bytes(nibs: bytes) -> bytes:
|
||||||
|
out = bytearray()
|
||||||
|
for i in range(0, len(nibs) - 1, 2):
|
||||||
|
out.append(((nibs[i] & 0x0F) << 4) | (nibs[i + 1] & 0x0F))
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_meta(frame: bytes) -> dict:
|
||||||
|
if len(frame) < 36 or frame[2] != 0x07 or frame[3] != 0x00:
|
||||||
|
raise ValueError(f"bad meta frame len={len(frame)}: {hexs(frame)}")
|
||||||
|
i = 4
|
||||||
|
valid = frame[i]
|
||||||
|
i += 1
|
||||||
|
ver = frame[i]
|
||||||
|
i += 1
|
||||||
|
size = u32_from_7bit(frame, i)
|
||||||
|
i += 5
|
||||||
|
data_size = u32_from_7bit(frame, i)
|
||||||
|
i += 5
|
||||||
|
write_off = u32_from_7bit(frame, i)
|
||||||
|
i += 5
|
||||||
|
wrap = u32_from_7bit(frame, i)
|
||||||
|
i += 5
|
||||||
|
boot = u32_from_7bit(frame, i)
|
||||||
|
i += 5
|
||||||
|
lines = u32_from_7bit(frame, i)
|
||||||
|
return {
|
||||||
|
"valid": valid,
|
||||||
|
"ver": ver,
|
||||||
|
"partition_size": size,
|
||||||
|
"data_size": data_size,
|
||||||
|
"write_off": write_off,
|
||||||
|
"wrap_count": wrap,
|
||||||
|
"boot_count": boot,
|
||||||
|
"line_count": lines,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_chunk(frame: bytes) -> tuple[int, bytes]:
|
||||||
|
if len(frame) < 11 or frame[2] != 0x07 or frame[3] != 0x01:
|
||||||
|
raise ValueError(f"bad chunk frame: {hexs(frame)}")
|
||||||
|
off = u32_from_7bit(frame, 4)
|
||||||
|
n = frame[9]
|
||||||
|
nibs = frame[10 : 10 + n]
|
||||||
|
if len(nibs) < n:
|
||||||
|
raise ValueError("truncated chunk")
|
||||||
|
return off, nibbles_to_bytes(nibs)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_send(sim: BleMidiSim, frame: bytes) -> None:
|
||||||
|
if not sim.is_connected():
|
||||||
|
raise RuntimeError("Not connected")
|
||||||
|
sim.send(frame)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_cmd(sim: BleMidiSim, cmd1: int, cmd2: int, timeout: float, verbose: bool):
|
||||||
|
t0 = time.monotonic()
|
||||||
|
others = []
|
||||||
|
while time.monotonic() - t0 < timeout:
|
||||||
|
if not sim.is_connected():
|
||||||
|
if verbose:
|
||||||
|
print(" (wait) link dropped")
|
||||||
|
return None
|
||||||
|
sim._pump()
|
||||||
|
i = 0
|
||||||
|
while i < len(sim.pending):
|
||||||
|
fr = sim.pending[i]
|
||||||
|
if (
|
||||||
|
len(fr) >= 4
|
||||||
|
and fr[0] == HEAD
|
||||||
|
and fr[1] == APP_ID
|
||||||
|
and fr[2] == cmd1
|
||||||
|
and fr[3] == cmd2
|
||||||
|
):
|
||||||
|
sim.pending.pop(i)
|
||||||
|
return fr
|
||||||
|
others.append(sim.pending.pop(i))
|
||||||
|
continue
|
||||||
|
time.sleep(0.01)
|
||||||
|
if verbose and others:
|
||||||
|
print(f" (timeout) saw {len(others)} other frame(s), last={hexs(others[-1][:32])}")
|
||||||
|
elif verbose:
|
||||||
|
print(" (timeout) no frames received at all")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def query_cmd(
|
||||||
|
sim: BleMidiSim,
|
||||||
|
frame: bytes,
|
||||||
|
cmd1: int,
|
||||||
|
cmd2: int,
|
||||||
|
timeout: float,
|
||||||
|
verbose: bool,
|
||||||
|
retries: int = 2,
|
||||||
|
):
|
||||||
|
# 轻量 drain,避免 read_frame 清空 pending 时误伤
|
||||||
|
t_drain = time.monotonic() + 0.15
|
||||||
|
while time.monotonic() < t_drain:
|
||||||
|
sim._pump()
|
||||||
|
sim.pending.clear()
|
||||||
|
time.sleep(0.02)
|
||||||
|
|
||||||
|
for attempt in range(retries + 1):
|
||||||
|
if verbose:
|
||||||
|
print(f" TX[{attempt}]: {hexs(frame)}")
|
||||||
|
try:
|
||||||
|
safe_send(sim, frame)
|
||||||
|
except Exception as e:
|
||||||
|
if verbose:
|
||||||
|
print(f" TX fail: {e}")
|
||||||
|
return None
|
||||||
|
fr = wait_cmd(sim, cmd1, cmd2, timeout=timeout, verbose=verbose)
|
||||||
|
if fr is not None:
|
||||||
|
if verbose:
|
||||||
|
print(f" RX: {hexs(fr[:48])}{'...' if len(fr) > 48 else ''}")
|
||||||
|
return fr
|
||||||
|
time.sleep(0.25)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def soft_probe(sim: BleMidiSim, verbose: bool = False) -> bool:
|
||||||
|
"""只用 01 01 探活,失败时不立刻 unpair(Windows 上 unpair 易把模组打哑巴)。"""
|
||||||
|
time.sleep(0.6) # 连接后给模组 settle
|
||||||
|
r = query_cmd(sim, build(0x01, 0x01), 0x01, 0x01, timeout=3.0, verbose=verbose, retries=2)
|
||||||
|
if r is not None and len(r) >= 5 and r[0] == HEAD and r[1] == APP_ID:
|
||||||
|
return True
|
||||||
|
if verbose:
|
||||||
|
print(" soft probe failed; try probe_or_rebind once")
|
||||||
|
try:
|
||||||
|
return bool(
|
||||||
|
sim.probe_or_rebind(
|
||||||
|
name=sim.info.get("name") or DEFAULT_NAME,
|
||||||
|
address=sim.info.get("address"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
if verbose:
|
||||||
|
print(f" probe_or_rebind exception: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def pull(sim: BleMidiSim, out_path: str, timeout: float = 4.0, verbose: bool = False) -> dict:
|
||||||
|
if not soft_probe(sim, verbose=verbose):
|
||||||
|
raise RuntimeError(
|
||||||
|
"BLE 协议无应答(01 01)。请确认琴已开机、蓝牙开关为开;"
|
||||||
|
"若刚用过 --transport uart 导致断连,请断电复位后再用 --transport midi。"
|
||||||
|
)
|
||||||
|
|
||||||
|
meta_frame = query_cmd(
|
||||||
|
sim, build(0x07, 0x00), 0x07, 0x00, timeout=timeout, verbose=verbose, retries=3
|
||||||
|
)
|
||||||
|
if meta_frame is None:
|
||||||
|
ver = query_cmd(
|
||||||
|
sim, build(0x01, 0x03), 0x01, 0x03, timeout=2.0, verbose=verbose, retries=1
|
||||||
|
)
|
||||||
|
tip = ""
|
||||||
|
if ver is not None:
|
||||||
|
tip = (
|
||||||
|
" 普通指令(01 03)正常,但 07 00 无应答 → 固件可能未含日志协议,请重新烧录。"
|
||||||
|
)
|
||||||
|
elif not sim.is_connected():
|
||||||
|
tip = " 链路已断开(uart 传输常见)。请改用: python ble_log_pull.py --transport midi ..."
|
||||||
|
raise RuntimeError("timeout waiting log meta (07 00)." + tip)
|
||||||
|
|
||||||
|
meta = parse_meta(meta_frame)
|
||||||
|
print(
|
||||||
|
f"meta valid={meta['valid']} ver={meta['ver']} size={meta['partition_size']} "
|
||||||
|
f"write={meta['write_off']} wrap={meta['wrap_count']} "
|
||||||
|
f"boot={meta['boot_count']} lines={meta['line_count']}"
|
||||||
|
)
|
||||||
|
if not meta["valid"] or meta["partition_size"] == 0:
|
||||||
|
raise RuntimeError("device log partition invalid")
|
||||||
|
|
||||||
|
total = meta["partition_size"]
|
||||||
|
buf = bytearray(b"\xff" * total)
|
||||||
|
got = 0
|
||||||
|
off = 0
|
||||||
|
while off < total:
|
||||||
|
want = min(CHUNK, total - off)
|
||||||
|
req = bytes([HEAD, APP_ID, 0x07, 0x01]) + u32_to_7bit(off) + bytes([want, 0xF7])
|
||||||
|
chunk = None
|
||||||
|
for attempt in range(4):
|
||||||
|
if not sim.is_connected():
|
||||||
|
raise RuntimeError(f"link dropped at offset 0x{off:X}")
|
||||||
|
try:
|
||||||
|
safe_send(sim, req)
|
||||||
|
except Exception as e:
|
||||||
|
time.sleep(0.1 * (attempt + 1))
|
||||||
|
if verbose:
|
||||||
|
print(f" chunk TX fail @0x{off:X}: {e}")
|
||||||
|
continue
|
||||||
|
fr = wait_cmd(sim, 0x07, 0x01, timeout=timeout, verbose=False)
|
||||||
|
if fr is None:
|
||||||
|
time.sleep(0.05 * (attempt + 1))
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
roff, data = parse_chunk(fr)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if roff == off and data:
|
||||||
|
chunk = data
|
||||||
|
break
|
||||||
|
if chunk is None:
|
||||||
|
raise RuntimeError(f"timeout at offset 0x{off:X} (after retries)")
|
||||||
|
buf[off : off + len(chunk)] = chunk
|
||||||
|
got += len(chunk)
|
||||||
|
off += len(chunk)
|
||||||
|
if off % 4096 == 0 or off >= total:
|
||||||
|
print(f" {off}/{total} ({100.0 * off / total:.1f}%)")
|
||||||
|
|
||||||
|
with open(out_path, "wb") as f:
|
||||||
|
f.write(buf)
|
||||||
|
print(f"wrote {out_path} ({got} bytes)")
|
||||||
|
return meta
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Pull K1 field log over BLE")
|
||||||
|
ap.add_argument("--out", default="LOG.BIN")
|
||||||
|
ap.add_argument("--name", default=DEFAULT_NAME)
|
||||||
|
ap.add_argument("--address", default=None)
|
||||||
|
ap.add_argument(
|
||||||
|
"--transport",
|
||||||
|
choices=("midi", "uart", "auto"),
|
||||||
|
default="midi",
|
||||||
|
help="默认 midi(uart 裸写在本模组上容易 Not connected)",
|
||||||
|
)
|
||||||
|
ap.add_argument("--decode", default=None, help="also decode to this text path")
|
||||||
|
ap.add_argument("--clear", action="store_true", help="clear flash log after pull")
|
||||||
|
ap.add_argument("-v", "--verbose", action="store_true")
|
||||||
|
ap.add_argument("--timeout", type=float, default=4.0)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
transports = [args.transport]
|
||||||
|
if args.transport == "auto":
|
||||||
|
# midi 优先:uart GATT 写可能导致模组断连
|
||||||
|
transports = ["midi", "uart"]
|
||||||
|
elif args.transport == "uart":
|
||||||
|
print(
|
||||||
|
"警告: --transport uart 在本机模组上常导致 Not connected;"
|
||||||
|
"若失败请改用 --transport midi"
|
||||||
|
)
|
||||||
|
|
||||||
|
last_err = None
|
||||||
|
for tr in transports:
|
||||||
|
print(f"=== transport={tr} ===")
|
||||||
|
sim = None
|
||||||
|
try:
|
||||||
|
sim = BleMidiSim(name=args.name, address=args.address, transport=tr)
|
||||||
|
print(f"connected: {sim.info}")
|
||||||
|
time.sleep(0.8)
|
||||||
|
if not sim.is_connected():
|
||||||
|
raise RuntimeError("connected then immediately dropped")
|
||||||
|
meta = pull(sim, args.out, timeout=args.timeout, verbose=args.verbose)
|
||||||
|
if args.decode:
|
||||||
|
from log_decode import decode_file
|
||||||
|
|
||||||
|
decode_file(
|
||||||
|
args.out,
|
||||||
|
args.decode,
|
||||||
|
meta.get("write_off", 0),
|
||||||
|
meta.get("wrap_count", 0),
|
||||||
|
)
|
||||||
|
if args.clear:
|
||||||
|
req = build(0x07, 0x02, 0x55, 0x2A)
|
||||||
|
safe_send(sim, req)
|
||||||
|
time.sleep(0.3)
|
||||||
|
sim._pump()
|
||||||
|
print("clear requested")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
last_err = e
|
||||||
|
print(f"FAILED ({tr}): {e}")
|
||||||
|
finally:
|
||||||
|
if sim is not None:
|
||||||
|
try:
|
||||||
|
sim.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
raise SystemExit(
|
||||||
|
f"all transports failed: {last_err}\n"
|
||||||
|
"建议: 1) 断电复位吉他 2) python ble_log_pull.py --transport midi -v --out LOG.BIN --decode crash.log\n"
|
||||||
|
"若有 J-Link: python rtt_flash_log_dump.py --out crash.log --scan"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Assemble portable K1 artist tone-update kit + zip for handoff."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import zipfile
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parents[3] # 一诺国际吉他 (tools->proj->Code->repo)
|
||||||
|
# __file__ = .../YNGJ.../tools/build_artist_kit.py → parents[0]=tools [1]=proj [2]=Code [3]=repo
|
||||||
|
PROJ = Path(__file__).resolve().parents[1]
|
||||||
|
TOOLS = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
# Fix REPO: tools -> project -> Code -> 一诺国际吉他
|
||||||
|
repo = TOOLS.parents[1] # Code's parent? TOOLS.parent=proj, TOOLS.parents[1]=Code, [2]=repo
|
||||||
|
repo = TOOLS.parents[2]
|
||||||
|
proj = TOOLS.parent
|
||||||
|
day = datetime.now().strftime("%Y%m%d")
|
||||||
|
kit_name = f"K1_音师音色更新工具_{day}"
|
||||||
|
out_root = repo / "tools" / "out"
|
||||||
|
out_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
kit = out_root / kit_name
|
||||||
|
if kit.exists():
|
||||||
|
shutil.rmtree(kit)
|
||||||
|
kit.mkdir(parents=True)
|
||||||
|
|
||||||
|
baseline = kit / "基线"
|
||||||
|
drop = kit / "投放"
|
||||||
|
outdir = kit / "输出"
|
||||||
|
baseline.mkdir()
|
||||||
|
drop.mkdir()
|
||||||
|
outdir.mkdir()
|
||||||
|
|
||||||
|
shutil.copy2(TOOLS / "tone_artist_pack.py", kit / "tone_artist_pack.py")
|
||||||
|
|
||||||
|
bat = """@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
setlocal
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo K1 音师音色更新工具 (USB / SoundWalkerIAP)
|
||||||
|
echo 依赖: 仅 Python 3,无需 pip / 无需 J-Link
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
where python >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [错误] 未找到 python。请安装 Python 3,安装时勾选 Add python.exe to PATH。
|
||||||
|
echo 下载: https://www.python.org/downloads/
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo 默认读取「投放」目录中的 1.bin 2.bin 3.bin
|
||||||
|
echo 也可传参,例如: 音师更新音色.bat --bin1 .\\1.bin --bin2 .\\2.bin --bin3 .\\3.bin
|
||||||
|
echo.
|
||||||
|
|
||||||
|
python "%~dp0tone_artist_pack.py" --open %*
|
||||||
|
set ERR=%ERRORLEVEL%
|
||||||
|
echo.
|
||||||
|
if not "%ERR%"=="0" (
|
||||||
|
echo 打包失败。查看: python "%~dp0tone_artist_pack.py" -h
|
||||||
|
pause
|
||||||
|
exit /b %ERR%
|
||||||
|
)
|
||||||
|
echo 完成。请打开「输出\\音师音色包_日期」按 请这样刷.txt 用 SoundWalkerIAP 刷机。
|
||||||
|
pause
|
||||||
|
exit /b 0
|
||||||
|
"""
|
||||||
|
(kit / "音师更新音色.bat").write_text(bat, encoding="utf-8", newline="\r\n")
|
||||||
|
|
||||||
|
ziliao = next(
|
||||||
|
p
|
||||||
|
for p in repo.iterdir()
|
||||||
|
if p.is_dir() and (p / "logo.bin").is_file() and (p / "Charg.bin").is_file()
|
||||||
|
)
|
||||||
|
shutil.copy2(ziliao / "logo.bin", baseline / "logo.bin")
|
||||||
|
shutil.copy2(ziliao / "Charg.bin", baseline / "Charg.bin")
|
||||||
|
shutil.copy2(proj / "tools" / "out" / "ui0902_res.bin", baseline / "ui0902_res.bin")
|
||||||
|
|
||||||
|
iap = repo / "升级" / "MCU主控升级"
|
||||||
|
shutil.copy2(iap / "SoundWalkerIAP.exe", kit / "SoundWalkerIAP.exe")
|
||||||
|
doc = iap / "升级步骤.docx"
|
||||||
|
if doc.is_file():
|
||||||
|
shutil.copy2(doc, kit / "升级步骤.docx")
|
||||||
|
|
||||||
|
src0909 = repo / "Doc" / "音色文件" / "0909"
|
||||||
|
for n in ("1.bin", "2.bin", "3.bin"):
|
||||||
|
shutil.copy2(src0909 / n, drop / n)
|
||||||
|
(drop / "说明.txt").write_text(
|
||||||
|
"请把新的 1.bin / 2.bin / 3.bin 放到本目录(覆盖即可),然后双击上一级「音师更新音色.bat」。\n"
|
||||||
|
"\n"
|
||||||
|
"也可用命令指定任意路径:\n"
|
||||||
|
" 音师更新音色.bat --bin1 路径\\1.bin --bin2 路径\\2.bin --bin3 路径\\3.bin\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
newline="\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
(kit / "使用说明.txt").write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"K1 音师音色更新工具 — 使用说明",
|
||||||
|
"================================",
|
||||||
|
"",
|
||||||
|
"【需要准备】",
|
||||||
|
" 1. 安装 Python 3(https://www.python.org/downloads/ ,勾选 Add to PATH)",
|
||||||
|
" 只需标准库,不用 pip 装任何包",
|
||||||
|
" 2. 本工具包(解压到任意目录,如桌面)",
|
||||||
|
" 3. 吉他 + USB 线(SoundWalkerIAP 升级,不需要 J-Link)",
|
||||||
|
"",
|
||||||
|
"【目录说明】",
|
||||||
|
" 投放\\ ← 把新的 1.bin 2.bin 3.bin 放这里",
|
||||||
|
" 基线\\ ← logo / Charg / UI 底图(勿改)",
|
||||||
|
" 输出\\ ← 打包结果(自动生成)",
|
||||||
|
" 音师更新音色.bat",
|
||||||
|
" tone_artist_pack.py",
|
||||||
|
" SoundWalkerIAP.exe",
|
||||||
|
" 升级步骤.docx",
|
||||||
|
" 使用说明.txt ← 本文件",
|
||||||
|
"",
|
||||||
|
"【日常步骤】",
|
||||||
|
" 1. 用新文件覆盖 投放\\1.bin、2.bin、3.bin",
|
||||||
|
" 2. 双击「音师更新音色.bat」",
|
||||||
|
" 3. 在弹出的「输出\\音师音色包_日期」里:",
|
||||||
|
" - 打开 SoundWalkerIAP.exe",
|
||||||
|
" - 吉他进入 USB 升级模式(见 升级步骤.docx)",
|
||||||
|
" - 擦除外部 Flash",
|
||||||
|
" - 把 extflash_ALL_artist_*.res 烧到地址 0x00000000",
|
||||||
|
" 4. 退出升级模式,重启试听",
|
||||||
|
"",
|
||||||
|
"【命令参数示例】",
|
||||||
|
" 音师更新音色.bat",
|
||||||
|
" 音师更新音色.bat --drop 投放",
|
||||||
|
" 音师更新音色.bat --bin1 .\\投放\\1.bin --bin2 .\\投放\\2.bin --bin3 .\\投放\\3.bin",
|
||||||
|
" 音师更新音色.bat --bin1 D:\\我的音色\\1.bin --bin2 D:\\我的音色\\2.bin --bin3 D:\\我的音色\\3.bin",
|
||||||
|
" python tone_artist_pack.py -h",
|
||||||
|
"",
|
||||||
|
"【生成的包格式】(与开发正式发布的 ALL.res 相同)",
|
||||||
|
" 0x00000000 logo.bin",
|
||||||
|
" 0x0000CB70 Charg.bin",
|
||||||
|
" 0x0001B8F0 1.bin(节奏)",
|
||||||
|
" 0x0009D07D 2.bin(本地曲,≤41KB)",
|
||||||
|
" 0x000A71AC 3.bin(万能)",
|
||||||
|
" 0x00100000 UI 界面图",
|
||||||
|
"",
|
||||||
|
"【注意】",
|
||||||
|
" - 必须刷 ALL.res,不要只刷单独的 1/2/3.bin(否则模式选择花屏)",
|
||||||
|
" - 一般只需刷外部 Flash,不用重刷 MCU 固件",
|
||||||
|
" - 若增删本地曲目或改曲名,需联系开发同步固件后再测",
|
||||||
|
" - 「投放」里已带示例 1/2/3.bin,可先双击 bat 试跑流程",
|
||||||
|
"",
|
||||||
|
"【出问题】",
|
||||||
|
" - 提示找不到 python → 安装 Python 3 并勾选 PATH,重开窗口",
|
||||||
|
" - DAB/大小校验失败 → 检查 bin 是否完整,2.bin 是否超过 41KB",
|
||||||
|
" - 其它问题把黑色窗口全文截图发给开发",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
newline="\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
zip_path = out_root / f"{kit_name}.zip"
|
||||||
|
if zip_path.exists():
|
||||||
|
zip_path.unlink()
|
||||||
|
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for f in kit.rglob("*"):
|
||||||
|
if f.is_file():
|
||||||
|
zf.write(f, f.relative_to(out_root).as_posix())
|
||||||
|
|
||||||
|
print(f"KIT {kit}")
|
||||||
|
print(f"ZIP {zip_path}")
|
||||||
|
print(f"ZIP size = {zip_path.stat().st_size} bytes")
|
||||||
|
for p in sorted(kit.iterdir()):
|
||||||
|
print(f" {p.name}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Flash APP via cspybat (AT32F403AC) then J-Link reset."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE = Path(r"C:\Users\qjyu\Documents\SoundWalker\一诺国际吉他\Code\YNGJ-GT1-M - AT32F403ARCT7")
|
||||||
|
STAGE = Path(r"C:\Temp\k1flash_boot")
|
||||||
|
CSPY = Path(r"C:\Program Files (x86)\IAR Systems\Embedded Workbench 7.3\common\bin\cspybat.exe")
|
||||||
|
JLINK = Path(r"C:\Program Files\SEGGER\JLink_V818\JLink.exe")
|
||||||
|
|
||||||
|
APP_OUT = BASE / "project" / "IAR_V7.4" / "YNGJ-GT1-M" / "Exe" / "YNGJ-GT1-M.out"
|
||||||
|
GEN_TMPL = BASE / "project" / "IAR_V7.4" / "settings" / "YNGJ-GT1-M.YNGJ-GT1-M.general.xcl"
|
||||||
|
DRV_TMPL = BASE / "project" / "IAR_V7.4" / "settings" / "YNGJ-GT1-M.YNGJ-GT1-M.driver.xcl"
|
||||||
|
RESET_JLINK = BASE / "tools" / "reset_run.jlink"
|
||||||
|
|
||||||
|
|
||||||
|
def kill_debuggers() -> None:
|
||||||
|
for n in (
|
||||||
|
"cspybat.exe", "CSpyBat.exe", "JLink.exe", "IarIdePm.exe",
|
||||||
|
"JLinkGUIServer.exe", "JLinkRTTClient.exe", "JFlash.exe",
|
||||||
|
):
|
||||||
|
subprocess.run(["taskkill", "/F", "/IM", n], capture_output=True)
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def stage_xcl(out_file: Path):
|
||||||
|
STAGE.mkdir(parents=True, exist_ok=True)
|
||||||
|
staged_out = STAGE / out_file.name
|
||||||
|
shutil.copy2(out_file, staged_out)
|
||||||
|
|
||||||
|
gen_lines = GEN_TMPL.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||||
|
new_gen = []
|
||||||
|
for ln in gen_lines:
|
||||||
|
if ".out" in ln and ("YNGJ" in ln or "BOOT" in ln or "AT32" in ln):
|
||||||
|
new_gen.append(f'"{staged_out}" ')
|
||||||
|
else:
|
||||||
|
new_gen.append(ln)
|
||||||
|
gen_path = STAGE / "general.xcl"
|
||||||
|
gen_path.write_text("\n".join(new_gen) + "\n", encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
|
drv_lines = DRV_TMPL.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||||
|
forced = [ln for ln in drv_lines if not ln.strip().startswith("--jlink_device")]
|
||||||
|
forced.append("--jlink_device=AT32F403AC")
|
||||||
|
drv_path = STAGE / "driver.xcl"
|
||||||
|
drv_path.write_text("\n".join(forced) + "\n", encoding="utf-8", newline="\n")
|
||||||
|
return gen_path, drv_path, staged_out
|
||||||
|
|
||||||
|
|
||||||
|
def cspy_download(out_file: Path, tag: str) -> None:
|
||||||
|
if not out_file.is_file():
|
||||||
|
raise SystemExit(f"missing {out_file}")
|
||||||
|
gen_path, drv_path, staged_out = stage_xcl(out_file)
|
||||||
|
logp = STAGE / f"cspy_{tag}.log"
|
||||||
|
cmd = [
|
||||||
|
str(CSPY), "-f", str(gen_path),
|
||||||
|
f"--debug_file={staged_out}",
|
||||||
|
"--download_only", "--backend", "-f", str(drv_path),
|
||||||
|
]
|
||||||
|
print(f"cspybat download {tag}: {staged_out.name}", flush=True)
|
||||||
|
with logp.open("w", encoding="utf-8", errors="replace") as log:
|
||||||
|
r = subprocess.run(cmd, stdout=log, stderr=subprocess.STDOUT, timeout=240)
|
||||||
|
text = logp.read_text(encoding="utf-8", errors="replace")
|
||||||
|
print(text[-1500:], flush=True)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise SystemExit(f"cspybat {tag} failed rc={r.returncode}")
|
||||||
|
|
||||||
|
|
||||||
|
def jlink_reset() -> None:
|
||||||
|
print("J-Link reset/run...", flush=True)
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
str(JLINK), "-Device", "AT32F403AC", "-If", "SWD",
|
||||||
|
"-Speed", "4000", "-AutoConnect", "1",
|
||||||
|
"-CommandFile", str(RESET_JLINK),
|
||||||
|
],
|
||||||
|
capture_output=True, timeout=45,
|
||||||
|
)
|
||||||
|
time.sleep(2.5)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
kill_debuggers()
|
||||||
|
cspy_download(APP_OUT, "app")
|
||||||
|
jlink_reset()
|
||||||
|
print("FLASH OK", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
HideDeviceSelection 1
|
||||||
|
si SWD
|
||||||
|
speed 4000
|
||||||
|
device AT32F403AC
|
||||||
|
connect
|
||||||
|
halt
|
||||||
|
loadbin project/IAR_V7.4/YNGJ-GT1-M/Exe/YNGJ-GT1-M.bin, 0x08008000
|
||||||
|
r
|
||||||
|
g
|
||||||
|
exit
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Flash Doc/音色文件/0903/3.bin to W25Q128 @ EXTFLASH_BIN3_UNIVERSAL_ADDR via RTT.
|
||||||
|
|
||||||
|
Requires firmware that accepts RTT command: flash bin3 <size>
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
DEFAULT_DEVICES = ("Cortex-M4", "AT32F403AC", "AT32F403A")
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
REPO = os.path.dirname(os.path.dirname(ROOT))
|
||||||
|
DEFAULT_BIN = os.path.join(REPO, "Doc", "音色文件", "0903", "3.bin")
|
||||||
|
EXTFLASH_BIN3_UNIVERSAL_ADDR = 0x000A71AC
|
||||||
|
DAB_MAGIC = bytes((0xAB, 0x44, 0x41, 0x42))
|
||||||
|
|
||||||
|
|
||||||
|
def connect_jlink(device: str):
|
||||||
|
import pylink
|
||||||
|
|
||||||
|
jlink = pylink.JLink()
|
||||||
|
jlink.open()
|
||||||
|
try:
|
||||||
|
jlink.exec_command("HideDeviceSelection = 1")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
||||||
|
|
||||||
|
last_error = None
|
||||||
|
for candidate in (device, *DEFAULT_DEVICES):
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
jlink.exec_command(f"Device = {candidate}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.connect(candidate)
|
||||||
|
print(f"Connected as {candidate}")
|
||||||
|
return jlink
|
||||||
|
except Exception as exc:
|
||||||
|
last_error = exc
|
||||||
|
jlink.close()
|
||||||
|
raise SystemExit(f"Failed to connect J-Link: {last_error}")
|
||||||
|
|
||||||
|
|
||||||
|
def find_rtt_control_block(jlink, ram_base: int = 0x20000000, ram_size: int = 0x18000):
|
||||||
|
needle = b"SEGGER RTT"
|
||||||
|
chunk = 0x1000
|
||||||
|
was_halted = jlink.halted()
|
||||||
|
if not was_halted:
|
||||||
|
jlink.halt()
|
||||||
|
try:
|
||||||
|
for off in range(0, ram_size, chunk):
|
||||||
|
data = bytes(jlink.memory_read8(ram_base + off, min(chunk, ram_size - off)))
|
||||||
|
idx = data.find(needle)
|
||||||
|
if idx >= 0:
|
||||||
|
return ram_base + off + idx
|
||||||
|
finally:
|
||||||
|
if not was_halted:
|
||||||
|
if hasattr(jlink, "restart"):
|
||||||
|
jlink.restart()
|
||||||
|
else:
|
||||||
|
jlink.go()
|
||||||
|
time.sleep(0.1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def rtt_read_text(jlink, timeout_s: float = 0.2) -> str:
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
chunks: list[bytes] = []
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
data = bytes(jlink.rtt_read(0, 512) or [])
|
||||||
|
except Exception:
|
||||||
|
data = b""
|
||||||
|
if data:
|
||||||
|
chunks.append(data)
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
else:
|
||||||
|
time.sleep(0.02)
|
||||||
|
return b"".join(chunks).decode("ascii", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for(jlink, marker: str, timeout_s: float = 30.0) -> str:
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
buf = ""
|
||||||
|
while time.time() < deadline:
|
||||||
|
buf += rtt_read_text(jlink, 0.15)
|
||||||
|
if marker in buf:
|
||||||
|
return buf
|
||||||
|
raise SystemExit(f"Timeout waiting for {marker!r}\n--- RTT ---\n{buf[-800:]}")
|
||||||
|
|
||||||
|
|
||||||
|
def send_bytes(jlink, payload: bytes) -> None:
|
||||||
|
off = 0
|
||||||
|
while off < len(payload):
|
||||||
|
try:
|
||||||
|
n = jlink.rtt_write(0, list(payload[off : off + 64]))
|
||||||
|
except Exception:
|
||||||
|
time.sleep(0.05)
|
||||||
|
continue
|
||||||
|
if n is None or n <= 0:
|
||||||
|
time.sleep(0.02)
|
||||||
|
continue
|
||||||
|
off += n
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--bin", default=DEFAULT_BIN)
|
||||||
|
parser.add_argument("--device", default="Cortex-M4")
|
||||||
|
parser.add_argument("--no-reset", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not os.path.isfile(args.bin):
|
||||||
|
raise SystemExit(f"missing bin: {args.bin}")
|
||||||
|
|
||||||
|
data = open(args.bin, "rb").read()
|
||||||
|
size = len(data)
|
||||||
|
if size < 16 or data[:4] != DAB_MAGIC:
|
||||||
|
raise SystemExit(f"not a DAB bank (magic={data[:4].hex()})")
|
||||||
|
|
||||||
|
print(f"bin={args.bin} size={size} addr=0x{EXTFLASH_BIN3_UNIVERSAL_ADDR:08X}")
|
||||||
|
|
||||||
|
jlink = connect_jlink(args.device)
|
||||||
|
try:
|
||||||
|
cb = find_rtt_control_block(jlink)
|
||||||
|
if cb is None:
|
||||||
|
raise SystemExit("RTT control block not found")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}")
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
time.sleep(0.3)
|
||||||
|
_ = rtt_read_text(jlink, 0.3)
|
||||||
|
|
||||||
|
if not args.no_reset:
|
||||||
|
print("hardware reset...")
|
||||||
|
jlink.reset(halt=False)
|
||||||
|
time.sleep(3.5)
|
||||||
|
try:
|
||||||
|
jlink.rtt_stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
cb = find_rtt_control_block(jlink)
|
||||||
|
if cb is None:
|
||||||
|
raise SystemExit("RTT CB missing after reset")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}")
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
time.sleep(0.5)
|
||||||
|
_ = rtt_read_text(jlink, 0.5)
|
||||||
|
|
||||||
|
cmd = f"flash bin3 {size}\n".encode("ascii")
|
||||||
|
send_bytes(jlink, cmd)
|
||||||
|
print("sent flash command, waiting GO...")
|
||||||
|
try:
|
||||||
|
log = wait_for(jlink, "FLASH_BIN3_GO", 20.0)
|
||||||
|
except SystemExit:
|
||||||
|
raise SystemExit(
|
||||||
|
"Firmware did not accept 'flash bin3'. Rebuild App with app_log.c "
|
||||||
|
"support, or temporarily flash via: concatenate 2.bin+3.bin and "
|
||||||
|
"'flash haitian <combined_size>'."
|
||||||
|
)
|
||||||
|
print(log.strip().splitlines()[-1])
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
chunk = 256
|
||||||
|
sent = 0
|
||||||
|
t0 = time.time()
|
||||||
|
while sent < size:
|
||||||
|
end = min(sent + chunk, size)
|
||||||
|
send_bytes(jlink, data[sent:end])
|
||||||
|
sent = end
|
||||||
|
log = wait_for(jlink, "FLASH_ACK", 60.0)
|
||||||
|
if "FLASH_BIN3_OK" in log:
|
||||||
|
print(log.strip().splitlines()[-1])
|
||||||
|
print("Done. Universal bank (3.bin) updated.")
|
||||||
|
return
|
||||||
|
if sent % (4 * 1024) == 0 or sent == size:
|
||||||
|
elapsed = time.time() - t0
|
||||||
|
print(f" {sent}/{size} ({100.0 * sent / size:.1f}%) {elapsed:.1f}s")
|
||||||
|
|
||||||
|
log = wait_for(jlink, "FLASH_BIN3_OK", 60.0)
|
||||||
|
print(log.strip().splitlines()[-1])
|
||||||
|
print("Done. Universal bank (3.bin) updated.")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
jlink.rtt_stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Flash Boot then APP via cspybat (AT32F403AC), reset, verify Boot strings."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE = Path(r"C:\Users\qjyu\Documents\SoundWalker\涓€璇哄浗闄呭悏浠朶Code\YNGJ-GT1-M - AT32F403ARCT7")
|
||||||
|
STAGE = Path(r"C:\Temp\k1flash_boot")
|
||||||
|
CSPY = Path(r"C:\Program Files (x86)\IAR Systems\Embedded Workbench 7.3\common\bin\cspybat.exe")
|
||||||
|
JLINK = Path(r"C:\Program Files\SEGGER\JLink_V818\JLink.exe")
|
||||||
|
|
||||||
|
BOOT_OUT = BASE / "AT32F403ARCT7_BOOT" / "project" / "IAR_V7.4" / "AT32F403ARCT7_BOOT" / "Exe" / "AT32F403ARCT7_BOOT.out"
|
||||||
|
APP_OUT = BASE / "project" / "IAR_V7.4" / "YNGJ-GT1-M" / "Exe" / "YNGJ-GT1-M.out"
|
||||||
|
GEN_TMPL = BASE / "project" / "IAR_V7.4" / "settings" / "YNGJ-GT1-M.YNGJ-GT1-M.general.xcl"
|
||||||
|
DRV_TMPL = BASE / "project" / "IAR_V7.4" / "settings" / "YNGJ-GT1-M.YNGJ-GT1-M.driver.xcl"
|
||||||
|
RESET_JLINK = BASE / "tools" / "reset_run.jlink"
|
||||||
|
|
||||||
|
NEW_GBK = bytes([0xC9, 0xD5, 0xC2, 0xBC, 0xC4, 0xA3, 0xCA, 0xBD]) # 鐑у綍妯″紡
|
||||||
|
OLD_GBK = bytes([0xC9, 0xFD, 0xBC, 0xB6, 0xC4, 0xA3, 0xCA, 0xBD]) # 鍗囩骇妯″紡
|
||||||
|
|
||||||
|
|
||||||
|
def kill_debuggers() -> None:
|
||||||
|
for n in (
|
||||||
|
"cspybat.exe",
|
||||||
|
"CSpyBat.exe",
|
||||||
|
"JLink.exe",
|
||||||
|
"IarIdePm.exe",
|
||||||
|
"JLinkGUIServer.exe",
|
||||||
|
"JLinkRTTClient.exe",
|
||||||
|
"JFlash.exe",
|
||||||
|
):
|
||||||
|
subprocess.run(["taskkill", "/F", "/IM", n], capture_output=True)
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def stage_xcl(out_file: Path) -> tuple[Path, Path]:
|
||||||
|
STAGE.mkdir(parents=True, exist_ok=True)
|
||||||
|
staged_out = STAGE / out_file.name
|
||||||
|
shutil.copy2(out_file, staged_out)
|
||||||
|
|
||||||
|
gen_lines = GEN_TMPL.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||||
|
new_gen = []
|
||||||
|
for ln in gen_lines:
|
||||||
|
if ".out" in ln and ("YNGJ" in ln or "BOOT" in ln or "AT32" in ln):
|
||||||
|
new_gen.append(f'"{staged_out}" ')
|
||||||
|
else:
|
||||||
|
new_gen.append(ln)
|
||||||
|
gen_path = STAGE / "general.xcl"
|
||||||
|
gen_path.write_text("\n".join(new_gen) + "\n", encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
|
drv_lines = DRV_TMPL.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||||
|
forced = [ln for ln in drv_lines if not ln.strip().startswith("--jlink_device")]
|
||||||
|
forced.append("--jlink_device=AT32F403AC")
|
||||||
|
drv_path = STAGE / "driver.xcl"
|
||||||
|
drv_path.write_text("\n".join(forced) + "\n", encoding="utf-8", newline="\n")
|
||||||
|
return gen_path, drv_path, staged_out
|
||||||
|
|
||||||
|
|
||||||
|
def cspy_download(out_file: Path, tag: str) -> None:
|
||||||
|
if not out_file.is_file():
|
||||||
|
raise SystemExit(f"missing {out_file}")
|
||||||
|
gen_path, drv_path, staged_out = stage_xcl(out_file)
|
||||||
|
logp = STAGE / f"cspy_{tag}.log"
|
||||||
|
cmd = [
|
||||||
|
str(CSPY),
|
||||||
|
"-f",
|
||||||
|
str(gen_path),
|
||||||
|
f"--debug_file={staged_out}",
|
||||||
|
"--download_only",
|
||||||
|
"--backend",
|
||||||
|
"-f",
|
||||||
|
str(drv_path),
|
||||||
|
]
|
||||||
|
print(f"cspybat download {tag}: {staged_out.name}", flush=True)
|
||||||
|
with logp.open("w", encoding="utf-8", errors="replace") as log:
|
||||||
|
r = subprocess.run(cmd, stdout=log, stderr=subprocess.STDOUT, timeout=240)
|
||||||
|
text = logp.read_text(encoding="utf-8", errors="replace")
|
||||||
|
print(text[-1500:], flush=True)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise SystemExit(f"cspybat {tag} failed rc={r.returncode}")
|
||||||
|
|
||||||
|
|
||||||
|
def jlink_reset() -> None:
|
||||||
|
print("J-Link reset/run...", flush=True)
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
str(JLINK),
|
||||||
|
"-Device",
|
||||||
|
"AT32F403AC",
|
||||||
|
"-If",
|
||||||
|
"SWD",
|
||||||
|
"-Speed",
|
||||||
|
"4000",
|
||||||
|
"-AutoConnect",
|
||||||
|
"1",
|
||||||
|
"-CommandFile",
|
||||||
|
str(RESET_JLINK),
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
timeout=45,
|
||||||
|
)
|
||||||
|
time.sleep(2.5)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_boot_in_mcu() -> None:
|
||||||
|
import pylink
|
||||||
|
|
||||||
|
j = pylink.JLink()
|
||||||
|
j.open()
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
j.exec_command("HideDeviceSelection = 1")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
j.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
||||||
|
last = None
|
||||||
|
for dev in ("AT32F403AC", "Cortex-M4", "AT32F403A"):
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
j.exec_command(f"Device = {dev}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
j.connect(dev)
|
||||||
|
print(f"verify connect: {dev}", flush=True)
|
||||||
|
break
|
||||||
|
except Exception as exc:
|
||||||
|
last = exc
|
||||||
|
else:
|
||||||
|
raise SystemExit(f"verify connect failed: {last}")
|
||||||
|
|
||||||
|
j.halt()
|
||||||
|
# Boot image is in first ~32KB; search GBK markers
|
||||||
|
chunk = bytes(j.memory_read8(0x08000000, 0x8000))
|
||||||
|
has_new = NEW_GBK in chunk
|
||||||
|
has_old = OLD_GBK in chunk
|
||||||
|
print(f"MCU@0x08000000 contains 鐑у綍妯″紡={has_new} 鍗囩骇妯″紡={has_old}", flush=True)
|
||||||
|
if not has_new:
|
||||||
|
raise SystemExit("Boot flash verify FAILED: new 鐑у綍妯″紡 string missing")
|
||||||
|
if has_old:
|
||||||
|
print("WARN: old 鍗囩骇妯″紡 string still present somewhere in Boot region", flush=True)
|
||||||
|
else:
|
||||||
|
print("Boot flash verify OK", flush=True)
|
||||||
|
j.reset(halt=False)
|
||||||
|
finally:
|
||||||
|
j.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
boot_bin = BOOT_OUT.with_suffix(".bin")
|
||||||
|
data = boot_bin.read_bytes()
|
||||||
|
print(
|
||||||
|
f"local BOOT.bin: new={NEW_GBK in data} old={OLD_GBK in data} size={len(data)}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
kill_debuggers()
|
||||||
|
cspy_download(BOOT_OUT, "boot")
|
||||||
|
kill_debuggers()
|
||||||
|
cspy_download(APP_OUT, "app")
|
||||||
|
jlink_reset()
|
||||||
|
verify_boot_in_mcu()
|
||||||
|
print("Done. Keep KEY for burn mode; screen should use UI0902_FLASH_MODE if ExtFlash has art.", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,289 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Flash K1 MCU APP (J-Link) + ExtFlash ALL.res (RTT flash all).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python tools/flash_mcu_and_extflash.py
|
||||||
|
python tools/flash_mcu_and_extflash.py --mcu PATH.bin --all PATH.res
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PROJ = Path(__file__).resolve().parents[1]
|
||||||
|
REPO = PROJ.parents[1]
|
||||||
|
STAGE = Path(r"C:\Temp\k1flash")
|
||||||
|
JLINK = Path(r"C:\Program Files\SEGGER\JLink_V818\JLink.exe")
|
||||||
|
DEFAULT_DEVICES = ("AT32F403AC", "Cortex-M4", "AT32F403A")
|
||||||
|
|
||||||
|
DEFAULT_MCU = (
|
||||||
|
REPO
|
||||||
|
/ "tools"
|
||||||
|
/ "out"
|
||||||
|
/ "K1_MCU_v0.2.11_toneRes_v0914_20260914_d06e44b"
|
||||||
|
/ "YNGJ-GT1-M_MCU_v0.2.11_20260914.bin"
|
||||||
|
)
|
||||||
|
FALLBACK_MCU = PROJ / "project" / "IAR_V7.4" / "YNGJ-GT1-M" / "Exe" / "YNGJ-GT1-M.bin"
|
||||||
|
|
||||||
|
DEFAULT_ALL = REPO / "tools" / "out" / "extflash_ALL_tone0914_ui0902.res"
|
||||||
|
FALLBACK_ALL = (
|
||||||
|
REPO
|
||||||
|
/ "tools"
|
||||||
|
/ "out"
|
||||||
|
/ "K1_MCU_v0.2.11_toneRes_v0914_20260914_d06e44b"
|
||||||
|
/ "extflash_ALL_tone0914_ui0902_20260914.res"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def kill_debuggers() -> None:
|
||||||
|
for n in (
|
||||||
|
"cspybat.exe",
|
||||||
|
"CSpyBat.exe",
|
||||||
|
"JLink.exe",
|
||||||
|
"JLinkGUIServer.exe",
|
||||||
|
"JLinkRTTClient.exe",
|
||||||
|
"JFlash.exe",
|
||||||
|
):
|
||||||
|
subprocess.run(["taskkill", "/F", "/IM", n], capture_output=True)
|
||||||
|
time.sleep(0.8)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_path(primary: Path, fallback: Path) -> Path:
|
||||||
|
if primary.is_file():
|
||||||
|
return primary
|
||||||
|
if fallback.is_file():
|
||||||
|
return fallback
|
||||||
|
raise SystemExit(f"missing file:\n {primary}\n {fallback}")
|
||||||
|
|
||||||
|
|
||||||
|
def flash_mcu_jlink(mcu_bin: Path) -> None:
|
||||||
|
STAGE.mkdir(parents=True, exist_ok=True)
|
||||||
|
staged = STAGE / "YNGJ-GT1-M.bin"
|
||||||
|
shutil.copy2(mcu_bin, staged)
|
||||||
|
cmdfile = STAGE / "flash_app.jlink"
|
||||||
|
cmdfile.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"HideDeviceSelection 1",
|
||||||
|
"si SWD",
|
||||||
|
"speed 4000",
|
||||||
|
"device AT32F403AC",
|
||||||
|
"connect",
|
||||||
|
"halt",
|
||||||
|
f"loadbin {staged}, 0x08008000",
|
||||||
|
"r",
|
||||||
|
"g",
|
||||||
|
"exit",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
encoding="ascii",
|
||||||
|
)
|
||||||
|
if not JLINK.is_file():
|
||||||
|
raise SystemExit(f"missing JLink.exe: {JLINK}")
|
||||||
|
print(f"[1/3] J-Link flash MCU → 0x08008000 ({staged.name}, {staged.stat().st_size} bytes)")
|
||||||
|
r = subprocess.run(
|
||||||
|
[
|
||||||
|
str(JLINK),
|
||||||
|
"-Device",
|
||||||
|
"AT32F403AC",
|
||||||
|
"-If",
|
||||||
|
"SWD",
|
||||||
|
"-Speed",
|
||||||
|
"4000",
|
||||||
|
"-AutoConnect",
|
||||||
|
"1",
|
||||||
|
"-CommandFile",
|
||||||
|
str(cmdfile),
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="replace",
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
out = (r.stdout or "") + (r.stderr or "")
|
||||||
|
print(out[-1200:] if out else f"rc={r.returncode}")
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise SystemExit(f"J-Link MCU flash failed rc={r.returncode}")
|
||||||
|
if "Failed" in out and "O.K." not in out:
|
||||||
|
raise SystemExit("J-Link MCU flash reported failure")
|
||||||
|
time.sleep(2.5)
|
||||||
|
|
||||||
|
|
||||||
|
def connect_jlink(device: str):
|
||||||
|
import pylink
|
||||||
|
|
||||||
|
jlink = pylink.JLink()
|
||||||
|
jlink.open()
|
||||||
|
try:
|
||||||
|
jlink.exec_command("HideDeviceSelection = 1")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
||||||
|
last = None
|
||||||
|
for candidate in (device, *DEFAULT_DEVICES):
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
jlink.exec_command(f"Device = {candidate}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.connect(candidate)
|
||||||
|
print(f"Connected as {candidate}")
|
||||||
|
return jlink
|
||||||
|
except Exception as exc:
|
||||||
|
last = exc
|
||||||
|
jlink.close()
|
||||||
|
raise SystemExit(f"J-Link connect failed: {last}")
|
||||||
|
|
||||||
|
|
||||||
|
def find_rtt_cb(jlink, ram_base: int = 0x20000000, ram_size: int = 0x18000):
|
||||||
|
needle = b"SEGGER RTT"
|
||||||
|
chunk = 0x1000
|
||||||
|
was_halted = jlink.halted()
|
||||||
|
if not was_halted:
|
||||||
|
jlink.halt()
|
||||||
|
try:
|
||||||
|
for off in range(0, ram_size, chunk):
|
||||||
|
data = bytes(jlink.memory_read8(ram_base + off, min(chunk, ram_size - off)))
|
||||||
|
idx = data.find(needle)
|
||||||
|
if idx >= 0:
|
||||||
|
return ram_base + off + idx
|
||||||
|
finally:
|
||||||
|
if not was_halted:
|
||||||
|
if hasattr(jlink, "restart"):
|
||||||
|
jlink.restart()
|
||||||
|
else:
|
||||||
|
jlink.go()
|
||||||
|
time.sleep(0.1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def rtt_read_text(jlink, timeout_s: float = 0.2) -> str:
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
chunks: list[bytes] = []
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
data = bytes(jlink.rtt_read(0, 512) or [])
|
||||||
|
except Exception:
|
||||||
|
data = b""
|
||||||
|
if data:
|
||||||
|
chunks.append(data)
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
else:
|
||||||
|
time.sleep(0.02)
|
||||||
|
return b"".join(chunks).decode("ascii", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for(jlink, marker: str, timeout_s: float) -> str:
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
buf = ""
|
||||||
|
while time.time() < deadline:
|
||||||
|
buf += rtt_read_text(jlink, 0.15)
|
||||||
|
if marker in buf:
|
||||||
|
return buf
|
||||||
|
raise SystemExit(f"Timeout waiting for {marker!r}\n--- RTT ---\n{buf[-1000:]}")
|
||||||
|
|
||||||
|
|
||||||
|
def send_bytes(jlink, payload: bytes) -> None:
|
||||||
|
off = 0
|
||||||
|
while off < len(payload):
|
||||||
|
try:
|
||||||
|
n = jlink.rtt_write(0, list(payload[off : off + 64]))
|
||||||
|
except Exception:
|
||||||
|
time.sleep(0.05)
|
||||||
|
continue
|
||||||
|
if n is None or n <= 0:
|
||||||
|
time.sleep(0.02)
|
||||||
|
continue
|
||||||
|
off += n
|
||||||
|
|
||||||
|
|
||||||
|
def flash_all_rtt(all_res: Path, device: str) -> None:
|
||||||
|
data = all_res.read_bytes()
|
||||||
|
size = len(data)
|
||||||
|
print(f"[2/3] RTT flash ALL.res @ 0x0 ({all_res.name}, {size} bytes)")
|
||||||
|
|
||||||
|
jlink = connect_jlink(device)
|
||||||
|
try:
|
||||||
|
print("hardware reset...")
|
||||||
|
jlink.reset(halt=False)
|
||||||
|
time.sleep(4.0)
|
||||||
|
cb = find_rtt_cb(jlink)
|
||||||
|
if cb is None:
|
||||||
|
raise SystemExit("RTT CB not found — is MCU running 0.2.11+ with APP_LOG?")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}")
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
time.sleep(0.5)
|
||||||
|
_ = rtt_read_text(jlink, 0.5)
|
||||||
|
|
||||||
|
send_bytes(jlink, f"flash all {size}\n".encode("ascii"))
|
||||||
|
log = wait_for(jlink, "FLASH_ALL_GO", 20.0)
|
||||||
|
print(log.strip().splitlines()[-1])
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
chunk = 256
|
||||||
|
sent = 0
|
||||||
|
t0 = time.time()
|
||||||
|
while sent < size:
|
||||||
|
end = min(sent + chunk, size)
|
||||||
|
send_bytes(jlink, data[sent:end])
|
||||||
|
sent = end
|
||||||
|
log = wait_for(jlink, "FLASH_ACK", 90.0)
|
||||||
|
if "FLASH_ALL_OK" in log:
|
||||||
|
print(log.strip().splitlines()[-1])
|
||||||
|
print(f"ALL.res done in {time.time() - t0:.1f}s")
|
||||||
|
break
|
||||||
|
if sent % (128 * 1024) == 0 or sent == size:
|
||||||
|
print(f" {sent}/{size} ({100.0 * sent / size:.1f}%) {time.time() - t0:.1f}s")
|
||||||
|
else:
|
||||||
|
log = wait_for(jlink, "FLASH_ALL_OK", 90.0)
|
||||||
|
print(log.strip().splitlines()[-1])
|
||||||
|
|
||||||
|
print("[3/3] verify BIN3 via tone status")
|
||||||
|
send_bytes(jlink, b"tone status\n")
|
||||||
|
log = wait_for(jlink, "TONE @BIN3", 10.0)
|
||||||
|
for line in log.strip().splitlines():
|
||||||
|
if "BIN3" in line or "TONE @" in line or "loaded" in line:
|
||||||
|
print(line)
|
||||||
|
if "magic=AB444142" in log.replace(" ", "") or "magic=AB" in log:
|
||||||
|
# formats like magic=AB444142 or magic=AB44...
|
||||||
|
pass
|
||||||
|
if "magic=00000000" in log:
|
||||||
|
raise SystemExit("BIN3 still empty after flash — verify failed")
|
||||||
|
print("Verify OK (BIN3 not empty).")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
jlink.rtt_stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--mcu", type=Path, default=DEFAULT_MCU)
|
||||||
|
ap.add_argument("--all", dest="all_res", type=Path, default=DEFAULT_ALL)
|
||||||
|
ap.add_argument("--device", default="AT32F403AC")
|
||||||
|
ap.add_argument("--skip-mcu", action="store_true")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
mcu = resolve_path(args.mcu, FALLBACK_MCU)
|
||||||
|
all_res = resolve_path(args.all_res, FALLBACK_ALL)
|
||||||
|
print(f"MCU : {mcu}")
|
||||||
|
print(f"ALL : {all_res}")
|
||||||
|
|
||||||
|
kill_debuggers()
|
||||||
|
if not args.skip_mcu:
|
||||||
|
flash_mcu_jlink(mcu)
|
||||||
|
else:
|
||||||
|
print("[1/3] skip MCU flash")
|
||||||
|
flash_all_rtt(all_res, args.device)
|
||||||
|
print("Done. Please power-cycle / long-press power and test universal pick.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -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())
|
||||||
|
|
@ -17,7 +17,9 @@ from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
REPO = ROOT.parent.parent
|
REPO = ROOT.parent.parent
|
||||||
CSV_PATH = REPO / "Doc" / "音色文件" / "0903" / "local_songs.csv"
|
CSV_PATH = REPO / "Doc" / "音色文件" / "0914" / "local_songs.csv"
|
||||||
|
if not CSV_PATH.is_file():
|
||||||
|
CSV_PATH = REPO / "Doc" / "音色文件" / "0903" / "local_songs.csv"
|
||||||
OUT_HDR = ROOT / "project" / "inc" / "LocalSongNames.h"
|
OUT_HDR = ROOT / "project" / "inc" / "LocalSongNames.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,14 @@ def rgba_to_rgb565(im):
|
||||||
|
|
||||||
|
|
||||||
def rgba_to_rgb565_opaque(im):
|
def rgba_to_rgb565_opaque(im):
|
||||||
"""Full-screen boot/charge: keep dark background pixels (no near-black punch-through)."""
|
"""Full-screen boot/charge/flash: keep dark bg; flatten RGBA onto black (not white).
|
||||||
im = im.convert("RGB")
|
|
||||||
|
Design exports like 烧录模式.png are mostly transparent with dark glyphs; a bare
|
||||||
|
convert('RGB') drops alpha and turns transparent into white — wrong on device.
|
||||||
|
"""
|
||||||
|
rgba = im.convert("RGBA")
|
||||||
|
bg = Image.new("RGBA", rgba.size, (0, 0, 0, 255))
|
||||||
|
im = Image.alpha_composite(bg, rgba).convert("RGB")
|
||||||
px = im.load()
|
px = im.load()
|
||||||
w, h = im.size
|
w, h = im.size
|
||||||
out = bytearray(w * h * 2)
|
out = bytearray(w * h * 2)
|
||||||
|
|
@ -131,14 +137,14 @@ INTERNAL = [
|
||||||
("gImage_UI0902_Volume_20x15", "02_顶部图标/音量图标-10.png"),
|
("gImage_UI0902_Volume_20x15", "02_顶部图标/音量图标-10.png"),
|
||||||
("gImage_UI0902_Bluetooth_11x17", "02_顶部图标/蓝牙图标-10.png"),
|
("gImage_UI0902_Bluetooth_11x17", "02_顶部图标/蓝牙图标-10.png"),
|
||||||
# BatteryFill generated by make_battery_fill() — not from mockup PNG
|
# BatteryFill generated by make_battery_fill() — not from mockup PNG
|
||||||
("gImage_UI0902_TabSetting_Sel_36x28", "03_底部图标/底部图标-10.png"),
|
("gImage_UI0902_TabSetting_Sel_40x32", "03_底部图标/底部图标-10.png"), # 万能白
|
||||||
("gImage_UI0902_TabSetting_Not_35x28", "03_底部图标/底部图标-11.png"),
|
("gImage_UI0902_TabSetting_Not_40x32", "03_底部图标/底部图标-11.png"), # 普通白
|
||||||
("gImage_UI0902_TabMixer_Sel_35x27", "03_底部图标/底部图标-12.png"),
|
("gImage_UI0902_TabMixer_Sel_40x32", "03_底部图标/底部图标-12.png"), # 专业白
|
||||||
("gImage_UI0902_TabMixer_Not_18x28", "03_底部图标/底部图标-13.png"),
|
("gImage_UI0902_TabMixer_Not_22x32", "03_底部图标/底部图标-13.png"), # 设置白
|
||||||
("gImage_UI0902_TabMode_Sel_36x28", "03_底部图标/底部图标-14.png"),
|
("gImage_UI0902_TabMode_Sel_40x32", "03_底部图标/底部图标-14.png"), # 万能蓝
|
||||||
("gImage_UI0902_TabMode_Not_35x29", "03_底部图标/底部图标-15.png"),
|
("gImage_UI0902_TabMode_Not_40x32", "03_底部图标/底部图标-15.png"), # 普通蓝
|
||||||
("gImage_UI0902_TabBack_Sel_35x27", "03_底部图标/底部图标-16.png"),
|
("gImage_UI0902_TabBack_Sel_40x32", "03_底部图标/底部图标-16.png"), # 专业蓝
|
||||||
("gImage_UI0902_TabBack_Not_18x28", "03_底部图标/底部图标-17.png"),
|
("gImage_UI0902_TabBack_Not_22x32", "03_底部图标/底部图标-17.png"), # 设置蓝
|
||||||
]
|
]
|
||||||
|
|
||||||
# ---------------- external flash (big images / text strips) ----------------
|
# ---------------- external flash (big images / text strips) ----------------
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
# k1_harness — K1 吉他统一 HIL 自动化测试
|
||||||
|
|
||||||
|
覆盖 K1 电吉他(YNGJ-GT1-M)的 **UI、按键、资源包、BLE、App 协议** 验证。
|
||||||
|
复用现有脚本,不重写用例:
|
||||||
|
|
||||||
|
- App SysEx 协议:`test_protocol_app_sim.run_tests`(COM)/ `test_protocol_ble.py`(BLE)
|
||||||
|
- RTT 注入/日志:`rtt_pitch_reg_test` 的 J-Link 连接与 `RttSession`
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
| 套件 | 硬件 |
|
||||||
|
|------|------|
|
||||||
|
| `app`(com) | USB 转串口接 UART4(BLE 桥),115200 8N1 |
|
||||||
|
| `app`(ble)/ `ble` | 笔记本蓝牙,设备广播名 `Smart Guitar MIDI` |
|
||||||
|
| `keys` / `keys1617` / `packs` / `ui` | J-Link + SWD,固件含 `app_log` 注入命令 |
|
||||||
|
|
||||||
|
依赖:`pip install pyserial bleak pylink-square pillow`
|
||||||
|
|
||||||
|
## 命令
|
||||||
|
|
||||||
|
在仓库根目录(推荐):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python run_k1_harness.py selftest
|
||||||
|
python run_k1_harness.py run --suite all --profile nightly
|
||||||
|
# 或
|
||||||
|
run_k1_harness.bat run --suite all --profile nightly
|
||||||
|
```
|
||||||
|
|
||||||
|
也可进入 `tools` 后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd tools
|
||||||
|
python -m k1_harness selftest
|
||||||
|
python -m k1_harness run --suite all --profile nightly
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注意:不要在仓库根目录直接执行 `python -m k1_harness`(会报 `No module named k1_harness`)。
|
||||||
|
> 包位于 `tools/k1_harness/`。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# App 协议(BLE 通道,默认)
|
||||||
|
python run_k1_harness.py run --suite app --transport ble
|
||||||
|
|
||||||
|
# App 协议(串口通道)
|
||||||
|
python run_k1_harness.py run --suite app --transport com --port COM5
|
||||||
|
|
||||||
|
# BLE 链路 smoke
|
||||||
|
python run_k1_harness.py run --suite ble
|
||||||
|
|
||||||
|
# 按键 / 资源包 / UI(J-Link RTT)
|
||||||
|
python run_k1_harness.py run --suite keys,keys1617,packs,ui
|
||||||
|
|
||||||
|
# 含关机用例(破坏性,跑完设备软关机)
|
||||||
|
python run_k1_harness.py run --suite all --allow-poweroff
|
||||||
|
```
|
||||||
|
|
||||||
|
报告默认写到 `Doc/reports/k1_hil_<时间戳>.md`,可用 `--report` 指定。
|
||||||
|
退出码:有 FAIL 为 1,否则 0。
|
||||||
|
|
||||||
|
## 套件说明
|
||||||
|
|
||||||
|
| 套件 | 覆盖 | 判定 |
|
||||||
|
|------|------|------|
|
||||||
|
| `app` | SysEx 组 01~06(设备信息/和弦映射/参数/LED/段落/电源) | ACK 帧头+长度+回读一致;04/06 无 ACK 记 SENT |
|
||||||
|
| `ble` | BLE 连接 + BLE-MIDI framing + smoke 往返 | 子进程 `test_protocol_ble.py --smoke` |
|
||||||
|
| `keys` | TM1629 和弦垫 key 1~23 全扫 + 移调 0/6/11 | `CHORD_KEY_OK` / `CHORD_XPOSE_OK` |
|
||||||
|
| `keys1617` | TM1617 段落/导航键 MAIN_D/C/B/A + 释放 | `TM1617_KEY_OK`(需新固件钩子) |
|
||||||
|
| `packs` | 1/2/3.bin 加载(地址 OK + ret=0)、拨片起奏、3.bin 尾奏回归 | `TONE_*` 行 + `[PITCH]` 活动 |
|
||||||
|
| `ui` | `log status` 页名、`TAP` 触摸注入、`ui boot/charge` 绘制、可选截屏 | 页名/ACK;TAP 需 `DEBUG_LCD_DUMP` 固件 |
|
||||||
|
|
||||||
|
## 固件测试钩子(RTT 下行命令,见 `APP/app_log.c`)
|
||||||
|
|
||||||
|
| 命令 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `chord key N` / `chord key N hold` | TM1629 注入;`hold` 保持不被物理空扫释放(`chord key 0` 解除) |
|
||||||
|
| `chord xpose N` | 移调 0~11 |
|
||||||
|
| `tm1617 key N` | TM1617 注入(0~3=MAIN_D/C/B/A,4=释放) |
|
||||||
|
| `adc key N on\|off` | ADC 键注入(1=独立尾奏键,与万能第4键同路径) |
|
||||||
|
| `tone bin1/bin2/bin3 [idx]` | 资源包加载校验 |
|
||||||
|
| `tone start` / `tone pick [uni]` | 拨片起奏 |
|
||||||
|
| `log status` / `log dump` / `sys reset` | 状态/日志/复位 |
|
||||||
|
| `TAP x y` / `DUMP` | 触摸注入/截屏(需 `DEBUG_LCD_DUMP`) |
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
|
||||||
|
- 手机 App 源码不在本仓;**App 功能验证 = BLE SysEx 端到端**(与真机 App 同协议)。
|
||||||
|
- 3.bin 尾奏回归对应音师需求 `Doc/音师需求_万能3.bin尾奏_20260914.md`:
|
||||||
|
尾奏触发后 4s 内应仍有 `[PITCH] on`,否则判定「一按立刻静音」FAIL。
|
||||||
|
- 老固件缺少新命令时相关用例记 SKIP 并在详情中提示重烧。
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""k1_harness — K1 吉他统一 HIL 自动化测试套件.
|
||||||
|
|
||||||
|
复用现有脚本的两根脊梁,不重写协议用例:
|
||||||
|
- App SysEx 协议: test_protocol_app_sim.run_tests (COM) / test_protocol_ble (BLE)
|
||||||
|
- RTT 注入/日志: rtt_pitch_reg_test 的 J-Link 连接与 RttSession
|
||||||
|
|
||||||
|
用法见 README.md 或: python -m k1_harness --help
|
||||||
|
"""
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from .cli import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""k1_harness CLI.
|
||||||
|
|
||||||
|
python -m k1_harness selftest
|
||||||
|
python -m k1_harness run --suite app --transport com --port COM5
|
||||||
|
python -m k1_harness run --suite app --transport ble
|
||||||
|
python -m k1_harness run --suite ble
|
||||||
|
python -m k1_harness run --suite keys,packs,ui --device AT32F403AC
|
||||||
|
python -m k1_harness run --suite all --profile nightly
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .paths import default_report_dir
|
||||||
|
from .report import HarnessReport
|
||||||
|
from .suites import SUITES, ALL_ORDER
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Ctx:
|
||||||
|
transport: str = "ble"
|
||||||
|
port: str = ""
|
||||||
|
baud: int = 115200
|
||||||
|
device: str = "AT32F403AC"
|
||||||
|
allow_poweroff: bool = False
|
||||||
|
dump_png: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _recover_device(device: str) -> None:
|
||||||
|
"""RTT 套件之间硬件复位,避免前序注入饿死后续命令。"""
|
||||||
|
import subprocess
|
||||||
|
from .paths import TOOLS_DIR
|
||||||
|
|
||||||
|
script = os.path.join(TOOLS_DIR, "rtt_reset.py")
|
||||||
|
print(f"\n----- 套件间复位 ({device}) -----", flush=True)
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, script, "--device", device, "--wait-boot", "5"],
|
||||||
|
timeout=60,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"复位失败(继续): {exc}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_selftest() -> int:
|
||||||
|
report = HarnessReport(title="K1 harness selftest(无硬件)")
|
||||||
|
|
||||||
|
# 1) 协议帧编解码自检(复用现有 selftest 逻辑,零硬件)
|
||||||
|
import io
|
||||||
|
import contextlib
|
||||||
|
import test_protocol_app_sim as appsim
|
||||||
|
|
||||||
|
buf = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(buf):
|
||||||
|
code = appsim.selftest()
|
||||||
|
for line in buf.getvalue().splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("[") and "]" in line:
|
||||||
|
status = line[1:line.index("]")].strip()
|
||||||
|
name = line[line.index("]") + 1:].split("|")[0].strip()
|
||||||
|
report.add("selftest", name, status if status in ("PASS", "FAIL", "SKIP", "SENT") else "FAIL")
|
||||||
|
report.add("selftest", "app_sim selftest 退出码", "PASS" if code == 0 else "FAIL", f"exit={code}")
|
||||||
|
|
||||||
|
# 2) pitch 分析器离线自检:喂一条合法/非法 PITCH 行
|
||||||
|
from .transports.rtt_session import analyze_lines
|
||||||
|
good = ("[PITCH] on ch8(bass) 52->40 d=-12 deg=3 chord=8 xp=2 xf=0 fl=0x03 FIX\n"
|
||||||
|
"[PITCH] on ch1(chord) 64->64 d=0 deg=1 chord=1 xp=0 xf=0 fl=0x00 PASS\n")
|
||||||
|
res = analyze_lines(good.splitlines())
|
||||||
|
report.add("selftest", "pitch 分析器(合法样本)", "PASS" if res.fail == 0 and res.ok >= 2 else "FAIL",
|
||||||
|
f"ok={res.ok} fail={res.fail}")
|
||||||
|
bad = "[PITCH] on ch8(bass) 52->30 d=-22 deg=3 chord=8 xp=2 xf=0 fl=0x03 FIX\n"
|
||||||
|
res2 = analyze_lines(bad.splitlines())
|
||||||
|
report.add("selftest", "pitch 分析器(非法样本检出)", "PASS" if res2.fail >= 1 else "FAIL",
|
||||||
|
f"fail={res2.fail}")
|
||||||
|
|
||||||
|
# 3) 报告落盘自检
|
||||||
|
out = os.path.join(default_report_dir(), "k1_selftest.md")
|
||||||
|
report.write_markdown(out)
|
||||||
|
report.print_summary()
|
||||||
|
return report.exit_code()
|
||||||
|
|
||||||
|
|
||||||
|
def _run_suites(args) -> int:
|
||||||
|
names = []
|
||||||
|
for part in args.suite.split(","):
|
||||||
|
part = part.strip().lower()
|
||||||
|
if part == "all":
|
||||||
|
names.extend(n for n in ALL_ORDER if n not in names)
|
||||||
|
elif part in SUITES:
|
||||||
|
if part not in names:
|
||||||
|
names.append(part)
|
||||||
|
else:
|
||||||
|
print(f"未知套件: {part}(可选: {', '.join(SUITES)} / all)", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
ctx = Ctx(
|
||||||
|
transport=args.transport,
|
||||||
|
port=args.port or "",
|
||||||
|
baud=args.baud,
|
||||||
|
device=args.device,
|
||||||
|
allow_poweroff=args.allow_poweroff or args.profile == "destructive",
|
||||||
|
dump_png=args.dump_png or "",
|
||||||
|
)
|
||||||
|
|
||||||
|
report = HarnessReport(title="K1 HIL 自动化测试报告")
|
||||||
|
report.notes.append(f"套件: {', '.join(names)}")
|
||||||
|
report.notes.append(f"transport={ctx.transport} device={ctx.device} profile={args.profile}")
|
||||||
|
|
||||||
|
prev_rtt = False
|
||||||
|
for name in names:
|
||||||
|
mod, needs = SUITES[name]
|
||||||
|
is_rtt = "rtt" in needs
|
||||||
|
if is_rtt and prev_rtt:
|
||||||
|
_recover_device(ctx.device)
|
||||||
|
if "transport" in needs and ctx.transport == "com" and not ctx.port:
|
||||||
|
report.add(name, "前置条件", "SKIP", "transport=com 需 --port;已跳过该套件")
|
||||||
|
continue
|
||||||
|
print(f"\n===== 套件 {name} =====", flush=True)
|
||||||
|
try:
|
||||||
|
mod.run(report, ctx)
|
||||||
|
except SystemExit as exc:
|
||||||
|
report.add(name, "套件执行", "FAIL", f"SystemExit: {exc}")
|
||||||
|
except Exception as exc: # 单套件异常不拖垮整体
|
||||||
|
report.add(name, "套件执行", "FAIL", f"{type(exc).__name__}: {exc}")
|
||||||
|
prev_rtt = is_rtt
|
||||||
|
|
||||||
|
report.print_summary()
|
||||||
|
ts = report.started.strftime("%Y%m%d_%H%M%S")
|
||||||
|
out = args.report or os.path.join(default_report_dir(), f"k1_hil_{ts}.md")
|
||||||
|
report.write_markdown(out)
|
||||||
|
return report.exit_code()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser(prog="k1_harness", description="K1 吉他统一 HIL 自动化测试")
|
||||||
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
sub.add_parser("selftest", help="无硬件自检(帧编解码 + 分析器 + 报告)")
|
||||||
|
|
||||||
|
runp = sub.add_parser("run", help="跑测试套件")
|
||||||
|
runp.add_argument("--suite", required=True,
|
||||||
|
help="逗号分隔: app,ble,keys,keys1617,packs,ui 或 all")
|
||||||
|
runp.add_argument("--transport", choices=("com", "ble"), default="ble",
|
||||||
|
help="app 套件通道(默认 ble)")
|
||||||
|
runp.add_argument("--port", default="", help="COM 端口(transport=com 时必填)")
|
||||||
|
runp.add_argument("--baud", type=int, default=115200)
|
||||||
|
runp.add_argument("--device", default="AT32F403AC", help="J-Link 器件名")
|
||||||
|
runp.add_argument("--profile", choices=("smoke", "nightly", "destructive"), default="nightly")
|
||||||
|
runp.add_argument("--allow-poweroff", action="store_true", help="允许 05 00 关机用例")
|
||||||
|
runp.add_argument("--dump-png", default="", help="ui 套件截屏输出路径(需 DEBUG_LCD_DUMP 固件)")
|
||||||
|
runp.add_argument("--report", default="", help="Markdown 报告输出路径")
|
||||||
|
|
||||||
|
args = ap.parse_args()
|
||||||
|
if args.cmd == "selftest":
|
||||||
|
raise SystemExit(_run_selftest())
|
||||||
|
raise SystemExit(_run_suites(args))
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""路径与 sys.path 引导:让 harness 能 import tools/ 下的现有脚本."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HARNESS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
TOOLS_DIR = os.path.dirname(HARNESS_DIR) # .../tools
|
||||||
|
REPO_DIR = os.path.dirname(TOOLS_DIR) # .../YNGJ-GT1-M - AT32F403ARCT7
|
||||||
|
WORKSPACE_DIR = os.path.dirname(os.path.dirname(REPO_DIR)) # .../一诺国际吉他
|
||||||
|
DOC_REPORTS_DIR = os.path.join(WORKSPACE_DIR, "Doc", "reports")
|
||||||
|
LOCAL_REPORTS_DIR = os.path.join(TOOLS_DIR, "out", "reports")
|
||||||
|
|
||||||
|
if TOOLS_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, TOOLS_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
def default_report_dir() -> str:
|
||||||
|
"""报告默认落到 Doc/reports(与工作区现有 BLE 报告一致);不存在则用 tools/out/reports."""
|
||||||
|
if os.path.isdir(os.path.join(WORKSPACE_DIR, "Doc")):
|
||||||
|
os.makedirs(DOC_REPORTS_DIR, exist_ok=True)
|
||||||
|
return DOC_REPORTS_DIR
|
||||||
|
os.makedirs(LOCAL_REPORTS_DIR, exist_ok=True)
|
||||||
|
return LOCAL_REPORTS_DIR
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from .markdown import CaseResult, HarnessReport
|
||||||
|
|
||||||
|
__all__ = ["CaseResult", "HarnessReport"]
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""统一测试报告:PASS/FAIL/SKIP/SENT 四态 + Markdown 输出 + 退出码."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
STATUSES = ("PASS", "FAIL", "SKIP", "SENT")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CaseResult:
|
||||||
|
suite: str
|
||||||
|
name: str
|
||||||
|
status: str
|
||||||
|
detail: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HarnessReport:
|
||||||
|
title: str = "K1 HIL 自动化测试报告"
|
||||||
|
cases: list[CaseResult] = field(default_factory=list)
|
||||||
|
notes: list[str] = field(default_factory=list)
|
||||||
|
started: datetime = field(default_factory=datetime.now)
|
||||||
|
|
||||||
|
def add(self, suite: str, name: str, status: str, detail: str = "") -> None:
|
||||||
|
status = status.upper()
|
||||||
|
assert status in STATUSES, f"bad status {status}"
|
||||||
|
self.cases.append(CaseResult(suite, name, status, detail))
|
||||||
|
print(f"[{status:>4}] {suite}/{name}" + (f" | {detail}" if detail else ""), flush=True)
|
||||||
|
|
||||||
|
def extend_rows(self, suite: str, rows) -> None:
|
||||||
|
"""吸收 test_protocol_app_sim.Results 风格的 (name, status, detail) 行."""
|
||||||
|
for name, status, detail in rows:
|
||||||
|
self.add(suite, name, status, detail)
|
||||||
|
|
||||||
|
def counts(self) -> dict:
|
||||||
|
return {s: sum(1 for c in self.cases if c.status == s) for s in STATUSES}
|
||||||
|
|
||||||
|
def exit_code(self) -> int:
|
||||||
|
return 1 if self.counts()["FAIL"] else 0
|
||||||
|
|
||||||
|
def summary_text(self) -> str:
|
||||||
|
n = self.counts()
|
||||||
|
return f"PASS={n['PASS']} FAIL={n['FAIL']} SKIP={n['SKIP']} SENT={n['SENT']}"
|
||||||
|
|
||||||
|
def print_summary(self) -> None:
|
||||||
|
print("\n===== 汇总 =====", flush=True)
|
||||||
|
for suite in dict.fromkeys(c.suite for c in self.cases):
|
||||||
|
sub = [c for c in self.cases if c.suite == suite]
|
||||||
|
n = {s: sum(1 for c in sub if c.status == s) for s in STATUSES}
|
||||||
|
print(f" {suite:<12} PASS={n['PASS']} FAIL={n['FAIL']} SKIP={n['SKIP']} SENT={n['SENT']}", flush=True)
|
||||||
|
print(f" {'TOTAL':<12} {self.summary_text()}", flush=True)
|
||||||
|
for c in self.cases:
|
||||||
|
if c.status == "FAIL":
|
||||||
|
print(f" FAIL: {c.suite}/{c.name} | {c.detail}", flush=True)
|
||||||
|
|
||||||
|
def write_markdown(self, path: str) -> str:
|
||||||
|
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
||||||
|
lines = [
|
||||||
|
f"# {self.title}",
|
||||||
|
"",
|
||||||
|
f"- 时间: {self.started.isoformat(timespec='seconds')}",
|
||||||
|
f"- 结果: {self.summary_text()}",
|
||||||
|
]
|
||||||
|
for note in self.notes:
|
||||||
|
lines.append(f"- {note}")
|
||||||
|
lines += ["", "| 套件 | 用例 | 结果 | 详情 |", "|------|------|------|------|"]
|
||||||
|
for c in self.cases:
|
||||||
|
detail = c.detail.replace("|", "\\|").replace("\n", " ")
|
||||||
|
lines.append(f"| {c.suite} | {c.name} | {c.status} | {detail} |")
|
||||||
|
lines.append("")
|
||||||
|
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write("\n".join(lines))
|
||||||
|
print(f"报告已保存: {os.path.abspath(path)}", flush=True)
|
||||||
|
return path
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""套件注册表。每个套件模块提供 run(report, ctx) -> None.
|
||||||
|
|
||||||
|
ctx 字段(cli 注入):
|
||||||
|
transport: "com" | "ble" —— app 套件使用
|
||||||
|
port: str —— COM 端口(transport=com 时必填)
|
||||||
|
baud: int
|
||||||
|
device: str —— J-Link 器件名
|
||||||
|
allow_poweroff: bool —— @destructive 用例开关
|
||||||
|
dump_png: str —— ui 套件可选截图输出路径
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from . import app_protocol, ble_smoke, keys_tm1629, keys_tm1617, packs_bin123, ui_nav
|
||||||
|
|
||||||
|
# name -> (module, 需要的资源标签)
|
||||||
|
SUITES = {
|
||||||
|
"app": (app_protocol, {"transport"}),
|
||||||
|
"ble": (ble_smoke, {"ble"}),
|
||||||
|
"keys": (keys_tm1629, {"rtt"}),
|
||||||
|
"keys1617": (keys_tm1617, {"rtt"}),
|
||||||
|
"packs": (packs_bin123, {"rtt"}),
|
||||||
|
"ui": (ui_nav, {"rtt"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
ALL_ORDER = ("app", "ble", "keys", "packs", "ui", "keys1617")
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""app 套件:App SysEx 全协议(组 01~06)。
|
||||||
|
|
||||||
|
- transport=com: 进程内复用 test_protocol_app_sim.run_tests(UART4 桥)
|
||||||
|
- transport=ble: 子进程跑 test_protocol_ble.py( bleak + BLE-MIDI framing),
|
||||||
|
解析其 [PASS]/[FAIL]/[SKIP]/[SENT] 输出行进统一报告
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ..paths import TOOLS_DIR
|
||||||
|
|
||||||
|
ROW_RE = re.compile(r"^\[(PASS|FAIL|SKIP|SENT)\s*\]\s*(.+?)(?:\s*\|\s*(.*))?$")
|
||||||
|
|
||||||
|
|
||||||
|
def _absorb_output(report, suite: str, text: str, stderr: str = "") -> None:
|
||||||
|
n = 0
|
||||||
|
for line in text.splitlines():
|
||||||
|
m = ROW_RE.match(line.strip())
|
||||||
|
if m:
|
||||||
|
report.add(suite, m.group(2), m.group(1), m.group(3) or "")
|
||||||
|
n += 1
|
||||||
|
if n == 0:
|
||||||
|
blob = (text or "") + "\n" + (stderr or "")
|
||||||
|
if "未找到设备" in blob or "扫描 BLE" in blob:
|
||||||
|
report.add(
|
||||||
|
suite,
|
||||||
|
"BLE 扫描连接",
|
||||||
|
"FAIL",
|
||||||
|
"未发现 Smart Guitar MIDI(确认已开机且蓝牙开;勿在软关机态跑 BLE)",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
tail = "\\n".join((text or "").splitlines()[-5:])
|
||||||
|
report.add(suite, "子进程输出解析", "FAIL", f"未找到结果行;tail: {tail}")
|
||||||
|
|
||||||
|
|
||||||
|
def run(report, ctx) -> None:
|
||||||
|
suite = "app"
|
||||||
|
if ctx.transport == "com":
|
||||||
|
if not ctx.port:
|
||||||
|
report.add(suite, "COM 连接", "FAIL", "transport=com 需要 --port COMx")
|
||||||
|
return
|
||||||
|
import test_protocol_app_sim as appsim
|
||||||
|
|
||||||
|
if appsim.serial is None:
|
||||||
|
report.add(suite, "pyserial", "FAIL", "pip install pyserial")
|
||||||
|
return
|
||||||
|
sim = appsim.AppSim(ctx.port, ctx.baud)
|
||||||
|
try:
|
||||||
|
res = appsim.run_tests(sim, allow_poweroff=ctx.allow_poweroff)
|
||||||
|
finally:
|
||||||
|
sim.close()
|
||||||
|
report.extend_rows(suite, res.rows)
|
||||||
|
return
|
||||||
|
|
||||||
|
# BLE:子进程调 test_protocol_ble.py(其内部已处理 bleak 异步/解配对/报告)
|
||||||
|
cmd = [
|
||||||
|
sys.executable, os.path.join(TOOLS_DIR, "test_protocol_ble.py"),
|
||||||
|
"--transport", "midi", "--no-unpair",
|
||||||
|
]
|
||||||
|
if ctx.allow_poweroff:
|
||||||
|
cmd.append("--allow-poweroff")
|
||||||
|
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
||||||
|
_absorb_output(report, suite, proc.stdout, proc.stderr or "")
|
||||||
|
if proc.returncode != 0 and not any(c.suite == suite and c.status == "FAIL" for c in report.cases):
|
||||||
|
report.add(suite, "BLE 协议子进程", "FAIL", f"exit={proc.returncode} {proc.stderr[-300:]}")
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""ble 套件:BLE 链路 smoke(连接 + BLE-MIDI framing + 少量协议往返)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ..paths import TOOLS_DIR
|
||||||
|
from .app_protocol import _absorb_output
|
||||||
|
|
||||||
|
|
||||||
|
def run(report, ctx) -> None:
|
||||||
|
suite = "ble"
|
||||||
|
cmd = [
|
||||||
|
sys.executable, os.path.join(TOOLS_DIR, "test_protocol_ble.py"),
|
||||||
|
"--transport", "midi", "--smoke", "--no-unpair",
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
report.add(suite, "BLE smoke", "FAIL", "timeout 180s(设备未广播/未配对?)")
|
||||||
|
return
|
||||||
|
before = len(report.cases)
|
||||||
|
_absorb_output(report, suite, proc.stdout, proc.stderr or "")
|
||||||
|
new = report.cases[before:]
|
||||||
|
if not any(c.status == "FAIL" for c in new) and proc.returncode != 0:
|
||||||
|
report.add(suite, "BLE smoke 退出码", "FAIL", f"exit={proc.returncode} {proc.stderr[-300:]}")
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""keys1617 套件:TM1617 段落/导航键注入(RTT `tm1617 key N`).
|
||||||
|
|
||||||
|
键值:0=KEY_MAIN_D 1=KEY_MAIN_C 2=KEY_MAIN_B 3=KEY_MAIN_A 4=释放(KEY_NULL)
|
||||||
|
每个键单独开 RTT 会话,失败时硬件复位后重试一次,避免触摸洪泛饿死后续命令。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ..paths import TOOLS_DIR
|
||||||
|
from ..transports.rtt_session import open_session
|
||||||
|
|
||||||
|
SUITE = "keys1617"
|
||||||
|
KEY_NAMES = {0: "MAIN_D", 1: "MAIN_C", 2: "MAIN_B", 3: "MAIN_A"}
|
||||||
|
|
||||||
|
|
||||||
|
def _hw_reset(device: str) -> None:
|
||||||
|
script = os.path.join(TOOLS_DIR, "rtt_reset.py")
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, script, "--device", device, "--wait-boot", "5"],
|
||||||
|
timeout=60,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _inject_once(sess, key: int) -> tuple[bool, str]:
|
||||||
|
tag = f"key={key}"
|
||||||
|
before = len(sess.lines)
|
||||||
|
ok = sess.cmd_ack(f"tm1617 key {key}", "TM1617_KEY_OK", timeout_s=2.5, retries=3)
|
||||||
|
if ok:
|
||||||
|
return True, "TM1617_KEY_OK"
|
||||||
|
recent = sess.lines[before:]
|
||||||
|
if any("tm1617 inject" in l and tag in l for l in recent):
|
||||||
|
return True, "inject log"
|
||||||
|
if any("TM1617_KEY_OK" in l and tag in l for l in recent):
|
||||||
|
return True, "TM1617_KEY_OK (late)"
|
||||||
|
return False, "无 ACK/inject 日志"
|
||||||
|
|
||||||
|
|
||||||
|
def _inject_with_retry(report, device: str, key: int, name: str) -> None:
|
||||||
|
try:
|
||||||
|
with open_session(device) as sess:
|
||||||
|
ok, detail = _inject_once(sess, key)
|
||||||
|
if ok:
|
||||||
|
# 尽快释放,减轻触摸洪泛
|
||||||
|
if key != 4:
|
||||||
|
sess.cmd_ack("tm1617 key 4", "TM1617_KEY_OK", timeout_s=1.5, retries=2)
|
||||||
|
time.sleep(0.3)
|
||||||
|
report.add(SUITE, name, "PASS", detail)
|
||||||
|
return
|
||||||
|
except SystemExit as exc:
|
||||||
|
detail = f"SystemExit: {exc}"
|
||||||
|
except Exception as exc:
|
||||||
|
detail = f"{type(exc).__name__}: {exc}"
|
||||||
|
|
||||||
|
print(f"!! {name} 首次失败,复位后重试…", flush=True)
|
||||||
|
_hw_reset(device)
|
||||||
|
try:
|
||||||
|
with open_session(device) as sess:
|
||||||
|
ok, detail = _inject_once(sess, key)
|
||||||
|
if ok:
|
||||||
|
if key != 4:
|
||||||
|
sess.cmd_ack("tm1617 key 4", "TM1617_KEY_OK", timeout_s=1.5, retries=2)
|
||||||
|
report.add(SUITE, name, "PASS", detail + " (retry)")
|
||||||
|
return
|
||||||
|
report.add(SUITE, name, "FAIL", detail)
|
||||||
|
except SystemExit as exc:
|
||||||
|
report.add(SUITE, name, "FAIL", f"retry SystemExit: {exc}")
|
||||||
|
except Exception as exc:
|
||||||
|
report.add(SUITE, name, "FAIL", f"retry {type(exc).__name__}: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
def run(report, ctx) -> None:
|
||||||
|
# 探测命令是否存在
|
||||||
|
try:
|
||||||
|
with open_session(ctx.device) as sess:
|
||||||
|
ok, detail = _inject_once(sess, 4)
|
||||||
|
except SystemExit as exc:
|
||||||
|
report.add(SUITE, "TM1617 注入命令探测", "FAIL", f"SystemExit: {exc}")
|
||||||
|
return
|
||||||
|
except Exception as exc:
|
||||||
|
report.add(SUITE, "TM1617 注入命令探测", "FAIL", f"{type(exc).__name__}: {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not ok:
|
||||||
|
report.add(SUITE, "TM1617 注入命令探测", "SKIP",
|
||||||
|
"无 TM1617_KEY_OK:固件缺少 'tm1617 key'(需含钩子固件)")
|
||||||
|
return
|
||||||
|
report.add(SUITE, "TM1617 注入命令探测", "PASS", detail)
|
||||||
|
|
||||||
|
for key, label in KEY_NAMES.items():
|
||||||
|
_inject_with_retry(report, ctx.device, key, f"tm1617 {label}")
|
||||||
|
_hw_reset(ctx.device) # 每键后复位,保证下一键 RTT 干净
|
||||||
|
|
||||||
|
_inject_with_retry(report, ctx.device, 4, "tm1617 释放(KEY_NULL)")
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""keys 套件:TM1629 和弦垫全键注入(RTT `chord key N`).
|
||||||
|
|
||||||
|
判定:CHORD_KEY_OK,或 [KEY] inject 日志(RTT 丢 ACK 时兜底)。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ..transports.rtt_session import open_session
|
||||||
|
|
||||||
|
SUITE = "keys"
|
||||||
|
|
||||||
|
|
||||||
|
def _chord_ok(sess, key: int) -> bool:
|
||||||
|
if sess.cmd_ack(f"chord key {key}", "CHORD_KEY_OK", timeout_s=2.5, retries=5):
|
||||||
|
return True
|
||||||
|
tag = f"inject key={key}"
|
||||||
|
return any(tag in line for line in sess.lines[-40:])
|
||||||
|
|
||||||
|
|
||||||
|
def run(report, ctx) -> None:
|
||||||
|
with open_session(ctx.device) as sess:
|
||||||
|
sess.cmd("log clear", 0.2)
|
||||||
|
|
||||||
|
if not _chord_ok(sess, 0):
|
||||||
|
report.add(SUITE, "TM1629 注入命令探测", "SKIP",
|
||||||
|
"无 CHORD_KEY_OK:固件缺少 'chord key' RTT 命令,请重烧新固件")
|
||||||
|
return
|
||||||
|
report.add(SUITE, "TM1629 注入命令探测", "PASS", "CHORD_KEY_OK")
|
||||||
|
|
||||||
|
for key in range(1, 24):
|
||||||
|
ok = _chord_ok(sess, key)
|
||||||
|
report.add(SUITE, f"chord key {key}", "PASS" if ok else "FAIL",
|
||||||
|
"" if ok else "无 ACK")
|
||||||
|
sess.pump(0.05)
|
||||||
|
|
||||||
|
ok = _chord_ok(sess, 0)
|
||||||
|
report.add(SUITE, "chord key 0 释放", "PASS" if ok else "FAIL", "" if ok else "无 ACK")
|
||||||
|
|
||||||
|
for xp in (0, 6, 11):
|
||||||
|
ok = sess.cmd_ack(f"chord xpose {xp}", "CHORD_XPOSE_OK", timeout_s=2.5, retries=5)
|
||||||
|
if not ok:
|
||||||
|
ok = any(f"xpose set {xp}" in l or f"xp={xp}" in l for l in sess.lines[-30:])
|
||||||
|
report.add(SUITE, f"chord xpose {xp}", "PASS" if ok else "FAIL", "" if ok else "无 ACK")
|
||||||
|
sess.cmd_ack("chord xpose 0", "CHORD_XPOSE_OK", timeout_s=2.0, retries=3)
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""packs 套件:资源包 1.bin/2.bin/3.bin 加载校验 + 拨片起奏 + 3.bin 尾奏回归.
|
||||||
|
|
||||||
|
- 加载: `tone bin1 N` / `tone bin2 N` / `tone bin3 N` → 期望 TONE_* 行 ret=0 且地址 OK
|
||||||
|
(`tone bin2` 依赖新固件命令;缺失时 2.bin 用例记 SKIP)
|
||||||
|
- 起奏: `tone start` 后 3s 内应有 [PITCH] on 事件
|
||||||
|
- 尾奏(音师需求 20260914): 万能 3.bin 起奏后 `adc key 1 on`(独立尾奏键,
|
||||||
|
与万能第4段落键同路径 AutoBand_StartOutro)→ 之后 4s 内仍应出现 [PITCH] on,
|
||||||
|
不再「一按立刻全静音」;结束 `adc key 1 off`
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ..transports.rtt_session import open_session, analyze_lines
|
||||||
|
|
||||||
|
SUITE = "packs"
|
||||||
|
|
||||||
|
BIN3_PRESETS = (0, 1, 2) # 1.1645 / 2.1564 / 3.1364
|
||||||
|
|
||||||
|
|
||||||
|
def _load_and_check(report, sess, cmd: str, tag: str, name: str, *, missing: str = "FAIL") -> bool:
|
||||||
|
"""发加载命令,校验 <tag> 行。True=有响应;False=无响应(按 missing 记结果)."""
|
||||||
|
sess.pump(0.2)
|
||||||
|
got = sess.cmd_ack(cmd, tag, timeout_s=4.0, retries=4)
|
||||||
|
if not got:
|
||||||
|
report.add(SUITE, name, missing, f"无 {tag} 响应")
|
||||||
|
return False
|
||||||
|
hit = [l for l in sess.lines[-20:] if tag in l]
|
||||||
|
line = hit[-1] if hit else tag
|
||||||
|
ok = "ret=0" in line and "OK" in line and "MISMATCH" not in line
|
||||||
|
if "ret=" not in line and tag in line:
|
||||||
|
ok = "MISMATCH" not in line and "ERR" not in line
|
||||||
|
report.add(SUITE, name, "PASS" if ok else "FAIL", line.strip())
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def run(report, ctx) -> None:
|
||||||
|
with open_session(ctx.device) as sess:
|
||||||
|
if not sess.cmd_ack("log status", "LOG_STATUS", timeout_s=3.0, retries=4):
|
||||||
|
report.add(SUITE, "RTT 下行探测", "FAIL", "log status 无响应(J-Link/固件忙?)")
|
||||||
|
return
|
||||||
|
sess.cmd("log clear", 0.3)
|
||||||
|
|
||||||
|
_load_and_check(report, sess, "tone bin1 0", "TONE_BIN1", "1.bin 节奏加载 idx0")
|
||||||
|
|
||||||
|
if not _load_and_check(report, sess, "tone bin2 0", "TONE_BIN2",
|
||||||
|
"2.bin 本地曲目加载 idx0", missing="SKIP"):
|
||||||
|
_load_and_check(report, sess, "tone local", "TONE_LOCAL",
|
||||||
|
"2.bin 本地曲目加载(tone local)", missing="SKIP")
|
||||||
|
|
||||||
|
bin3_ok = True
|
||||||
|
for idx in BIN3_PRESETS:
|
||||||
|
if not _load_and_check(report, sess, f"tone bin3 {idx}", "TONE_BIN3",
|
||||||
|
f"3.bin 万能加载 idx{idx}"):
|
||||||
|
bin3_ok = False
|
||||||
|
|
||||||
|
if not bin3_ok:
|
||||||
|
report.add(SUITE, "拨片起奏/尾奏", "SKIP", "3.bin 加载失败,级联跳过")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ---- 拨片起奏(万能 idx0)----
|
||||||
|
sess.cmd("log clear", 0.2)
|
||||||
|
sess.cmd_ack("tone bin3 0", "TONE_BIN3", timeout_s=4.0, retries=3)
|
||||||
|
base = len(sess.lines)
|
||||||
|
sess.cmd_ack("tone start", "TONE_START_DONE", timeout_s=3.0, retries=3)
|
||||||
|
sess.pump(3.0)
|
||||||
|
pick_lines = sess.lines[base:]
|
||||||
|
pitch_on = [l for l in pick_lines if "[PITCH]" in l and " on " in l]
|
||||||
|
report.add(SUITE, "3.bin 拨片起奏有声", "PASS" if pitch_on else "FAIL",
|
||||||
|
f"{len(pitch_on)} 条 PITCH on" if pitch_on else "起奏后无 PITCH on")
|
||||||
|
|
||||||
|
# ---- 尾奏回归 ----
|
||||||
|
if not sess.cmd_ack("adc key 1 on", "ADC_KEY_OK", timeout_s=2.5, retries=4):
|
||||||
|
# 兜底:万能第 4 键(固件已映射到 AutoBand_StartOutro)
|
||||||
|
if sess.cmd_ack("chord key 4", "CHORD_KEY_OK", timeout_s=2.0, retries=3):
|
||||||
|
report.add(SUITE, "3.bin 尾奏触发", "PASS", "chord key 4(adc key 不可用)")
|
||||||
|
else:
|
||||||
|
report.add(SUITE, "3.bin 尾奏(adc key 1)", "SKIP",
|
||||||
|
"固件缺少 'adc key';chord key 4 也无 ACK")
|
||||||
|
sess.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
report.add(SUITE, "3.bin 尾奏触发", "PASS", "ADC_KEY_OK")
|
||||||
|
|
||||||
|
out_base = len(sess.lines)
|
||||||
|
sess.pump(4.0)
|
||||||
|
outro_lines = sess.lines[out_base:]
|
||||||
|
outro_pitch = [l for l in outro_lines if "[PITCH]" in l and " on " in l]
|
||||||
|
stopped = any(("Stop_AutoBand" in l) or ("stop" in l.lower() and "AUTOBAND" in l)
|
||||||
|
for l in outro_lines)
|
||||||
|
if outro_pitch:
|
||||||
|
report.add(SUITE, "3.bin 尾奏有声(非立刻静音)", "PASS",
|
||||||
|
f"尾奏触发后 {len(outro_pitch)} 条 PITCH on")
|
||||||
|
elif stopped:
|
||||||
|
report.add(SUITE, "3.bin 尾奏有声(非立刻静音)", "FAIL",
|
||||||
|
"尾奏触发后立刻停止(3.bin Postamble 数据为空?见音师需求 20260914)")
|
||||||
|
else:
|
||||||
|
report.add(SUITE, "3.bin 尾奏有声(非立刻静音)", "FAIL",
|
||||||
|
"尾奏触发后无任何 PITCH on")
|
||||||
|
sess.cmd_ack("adc key 1 off", "ADC_KEY_OK", timeout_s=2.0, retries=2)
|
||||||
|
sess.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
|
||||||
|
|
||||||
|
res = analyze_lines(sess.lines)
|
||||||
|
if res.fail:
|
||||||
|
report.add(SUITE, "音区映射断言", "FAIL", f"{res.fail} 条 FAIL(详见日志)")
|
||||||
|
elif res.ok:
|
||||||
|
report.add(SUITE, "音区映射断言", "PASS", f"{res.ok} OK / {res.skip} SKIP")
|
||||||
|
else:
|
||||||
|
report.add(SUITE, "音区映射断言", "SKIP", "无可分析 PITCH 样本")
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""ui 套件:UI 页面状态 + 触摸注入导航 + 可选截屏.
|
||||||
|
|
||||||
|
- `log status` → 解析 ui=<page>
|
||||||
|
- `TAP x y`(需 DEBUG_LCD_DUMP)→ "TAP inject";缺失记 SKIP
|
||||||
|
- 可选 --dump-png:截屏留证(不作像素门禁)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ..paths import TOOLS_DIR
|
||||||
|
from ..transports.rtt_session import open_session
|
||||||
|
|
||||||
|
SUITE = "ui"
|
||||||
|
UI_RE = re.compile(r"ui=(\S+)")
|
||||||
|
|
||||||
|
|
||||||
|
def _read_ui_page(sess) -> str | None:
|
||||||
|
if not sess.cmd_ack("log status", "LOG_STATUS", timeout_s=3.0, retries=5):
|
||||||
|
return None
|
||||||
|
for line in reversed(sess.lines[-40:]):
|
||||||
|
if "LOG_STATUS" not in line:
|
||||||
|
continue
|
||||||
|
m = UI_RE.search(line)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def run(report, ctx) -> None:
|
||||||
|
with open_session(ctx.device) as sess:
|
||||||
|
if not sess.cmd_ack("log status", "LOG_STATUS", timeout_s=3.0, retries=5):
|
||||||
|
report.add(SUITE, "RTT 下行探测", "FAIL",
|
||||||
|
"log status 无响应(前序套件可能挤占 RTT;复位设备后单跑 --suite ui)")
|
||||||
|
return
|
||||||
|
|
||||||
|
page0 = _read_ui_page(sess)
|
||||||
|
if page0:
|
||||||
|
report.add(SUITE, "UI 页面上报", "PASS", f"ui={page0}")
|
||||||
|
else:
|
||||||
|
# 已确认 LOG_STATUS 可达;缺 ui= 字段时仍记 PASS(旧固件)但带详情
|
||||||
|
hit = [l for l in sess.lines[-20:] if "LOG_STATUS" in l]
|
||||||
|
if hit:
|
||||||
|
report.add(SUITE, "UI 页面上报", "PASS",
|
||||||
|
f"LOG_STATUS 可达(无 ui= 字段): {hit[-1].strip()}")
|
||||||
|
else:
|
||||||
|
report.add(SUITE, "UI 页面上报", "FAIL", "log status 无 ui= 字段")
|
||||||
|
|
||||||
|
tap_ack = sess.cmd_ack("TAP 120 80", "TAP inject", timeout_s=2.0, retries=2)
|
||||||
|
if not tap_ack:
|
||||||
|
report.add(SUITE, "触摸注入 TAP", "SKIP",
|
||||||
|
"无 'TAP inject':固件未开 DEBUG_LCD_DUMP(发布固件默认关闭)")
|
||||||
|
else:
|
||||||
|
report.add(SUITE, "触摸注入 TAP", "PASS", "TAP inject")
|
||||||
|
sess.pump(1.0)
|
||||||
|
page1 = _read_ui_page(sess)
|
||||||
|
if page1:
|
||||||
|
report.add(SUITE, "TAP 后页面状态", "PASS", f"ui={page0} -> {page1}")
|
||||||
|
else:
|
||||||
|
hit = [l for l in sess.lines[-20:] if "LOG_STATUS" in l]
|
||||||
|
if hit:
|
||||||
|
report.add(SUITE, "TAP 后页面状态", "PASS",
|
||||||
|
f"LOG_STATUS 可达: {hit[-1].strip()}")
|
||||||
|
else:
|
||||||
|
report.add(SUITE, "TAP 后页面状态", "FAIL", "TAP 后 log status 无响应")
|
||||||
|
|
||||||
|
for cmd, ack, name, to in (
|
||||||
|
("ui boot", "UI_BOOT_OK", "开机画面绘制", 5.0),
|
||||||
|
("ui charge", "UI_CHARGE_", "充电画面绘制", 15.0),
|
||||||
|
):
|
||||||
|
ok = sess.cmd_ack(cmd, ack, timeout_s=to, retries=3)
|
||||||
|
report.add(SUITE, name, "PASS" if ok else "FAIL", "" if ok else f"无 {ack}*")
|
||||||
|
|
||||||
|
if tap_ack:
|
||||||
|
sess.cmd("TAP 20 20", settle=0.5)
|
||||||
|
|
||||||
|
if ctx.dump_png:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, os.path.join(TOOLS_DIR, "rtt_lcd_capture.py"),
|
||||||
|
"--out", ctx.dump_png, "--device", ctx.device],
|
||||||
|
capture_output=True, text=True, timeout=120)
|
||||||
|
ok = proc.returncode == 0 and os.path.isfile(ctx.dump_png)
|
||||||
|
report.add(SUITE, "LCD 截屏", "PASS" if ok else "SKIP",
|
||||||
|
ctx.dump_png if ok else "截屏失败(需 DEBUG_LCD_DUMP 固件)")
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""RTT 传输:薄封装 rtt_pitch_reg_test 的 J-Link/RTT 实现(单一事实来源,避免复制)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from ..paths import TOOLS_DIR # noqa: F401 (确保 sys.path 已注入)
|
||||||
|
|
||||||
|
import rtt_pitch_reg_test as _rtt
|
||||||
|
|
||||||
|
# 直接复用,保持零行为变更
|
||||||
|
connect_jlink = _rtt.connect_jlink
|
||||||
|
find_rtt_control_block = _rtt.find_rtt_control_block
|
||||||
|
wait_rtt_ready = _rtt.wait_rtt_ready
|
||||||
|
RttSession = _rtt.RttSession
|
||||||
|
analyze_lines = _rtt.analyze_lines
|
||||||
|
CheckResult = _rtt.CheckResult
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def open_session(device: str = "AT32F403AC", *, reset: bool = True):
|
||||||
|
"""连接 J-Link + 启动 RTT,yield RttSession;退出时清理.
|
||||||
|
|
||||||
|
reset=True:先硬件复位并等待开机,避免前序 LCD/烧写把主循环卡死导致无 ACK。
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
_rtt.import_deps()
|
||||||
|
jlink = connect_jlink(device)
|
||||||
|
try:
|
||||||
|
if reset:
|
||||||
|
try:
|
||||||
|
jlink.reset(halt=False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(5.0)
|
||||||
|
cb = find_rtt_control_block(jlink)
|
||||||
|
if cb is None:
|
||||||
|
raise SystemExit("SEGGER RTT control block 未找到(固件未运行?)")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
wait_rtt_ready(jlink)
|
||||||
|
sess = RttSession(jlink)
|
||||||
|
sess.pump(0.3)
|
||||||
|
yield sess
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
jlink.rtt_stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.close()
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
log_decode.py — 将 K1 LOG.BIN(W25Q 日志分区镜像)解码为可读时间线
|
||||||
|
|
||||||
|
布局:
|
||||||
|
[0x0000 .. 0x0FFF] header (magic K1LG ...)
|
||||||
|
[0x1000 .. end] 文本环形区,行以 \\n 结束
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python log_decode.py LOG.BIN -o problem.log
|
||||||
|
python log_decode.py LOG.BIN --write-off 1234 --wrap 1 -o problem.log
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import struct
|
||||||
|
|
||||||
|
MAGIC = 0x4B314C47 # 'K1LG'
|
||||||
|
HDR_SIZE = 0x1000
|
||||||
|
|
||||||
|
|
||||||
|
def unpack_header(blob: bytes) -> dict:
|
||||||
|
if len(blob) < 36:
|
||||||
|
return {"valid": False}
|
||||||
|
magic = struct.unpack_from("<I", blob, 0)[0]
|
||||||
|
ver = struct.unpack_from("<H", blob, 4)[0]
|
||||||
|
boot = struct.unpack_from("<I", blob, 8)[0]
|
||||||
|
write_off = struct.unpack_from("<I", blob, 12)[0]
|
||||||
|
wrap = struct.unpack_from("<I", blob, 16)[0]
|
||||||
|
lines = struct.unpack_from("<I", blob, 20)[0]
|
||||||
|
overflow = struct.unpack_from("<I", blob, 24)[0]
|
||||||
|
seq = struct.unpack_from("<I", blob, 28)[0]
|
||||||
|
return {
|
||||||
|
"valid": magic == MAGIC,
|
||||||
|
"magic": magic,
|
||||||
|
"ver": ver,
|
||||||
|
"boot_count": boot,
|
||||||
|
"write_off": write_off,
|
||||||
|
"wrap_count": wrap,
|
||||||
|
"line_count": lines,
|
||||||
|
"overflow": overflow,
|
||||||
|
"seq": seq,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def extract_text(data: bytes, write_off: int, wrapped: bool) -> str:
|
||||||
|
"""Linearize ring: if wrapped, [write_off..end) + [0..write_off); else [0..write_off)."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
if write_off > len(data):
|
||||||
|
write_off = len(data)
|
||||||
|
if wrapped and write_off < len(data):
|
||||||
|
raw = data[write_off:] + data[:write_off]
|
||||||
|
else:
|
||||||
|
raw = data[:write_off] if write_off else data
|
||||||
|
text = raw.decode("utf-8", errors="replace")
|
||||||
|
text = text.replace("\xff", "")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def decode_file(bin_path: str, out_path: str, write_off=None, wrap_count=None) -> None:
|
||||||
|
with open(bin_path, "rb") as f:
|
||||||
|
blob = f.read()
|
||||||
|
hdr = unpack_header(blob)
|
||||||
|
data = blob[HDR_SIZE:] if len(blob) > HDR_SIZE else b""
|
||||||
|
|
||||||
|
wo = write_off if write_off is not None else hdr.get("write_off", 0)
|
||||||
|
wrap = wrap_count if wrap_count is not None else hdr.get("wrap_count", 0)
|
||||||
|
|
||||||
|
text = extract_text(data, wo, wrap > 0)
|
||||||
|
lines = [ln for ln in text.splitlines() if ln.strip()]
|
||||||
|
|
||||||
|
with open(out_path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(
|
||||||
|
f"# K1 log decode magic_ok={hdr.get('valid')} ver={hdr.get('ver')} "
|
||||||
|
f"boot={hdr.get('boot_count')} write_off={wo} wrap={wrap} "
|
||||||
|
f"line_count={hdr.get('line_count')} decoded_lines={len(lines)}\n"
|
||||||
|
)
|
||||||
|
for ln in lines:
|
||||||
|
f.write(ln.rstrip("\r") + "\n")
|
||||||
|
print(f"decoded {len(lines)} lines -> {out_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("bin")
|
||||||
|
ap.add_argument("-o", "--out", default="problem.log")
|
||||||
|
ap.add_argument("--write-off", type=int, default=None)
|
||||||
|
ap.add_argument("--wrap", type=int, default=None)
|
||||||
|
args = ap.parse_args()
|
||||||
|
decode_file(args.bin, args.out, args.write_off, args.wrap)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,387 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Laptop-mic pitch analyzer for K1 accompaniment register checks.
|
||||||
|
|
||||||
|
Uses autocorrelation F0 estimate (numpy only + sounddevice).
|
||||||
|
Helps verify bass/chord octave when MIDI channel logs lack ch8(bass).
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
python tools/mic_pitch_analyze.py --list
|
||||||
|
python tools/mic_pitch_analyze.py --seconds 8 --out mic_i.log
|
||||||
|
python tools/mic_pitch_analyze.py --live
|
||||||
|
python tools/mic_pitch_analyze.py --compare mic_i.wav mic_iii.wav
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import wave
|
||||||
|
from collections import Counter
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
try:
|
||||||
|
import sounddevice as sd
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit("Missing sounddevice: pip install sounddevice") from exc
|
||||||
|
|
||||||
|
A4_HZ = 440.0
|
||||||
|
A4_MIDI = 69
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PitchFrame:
|
||||||
|
t: float
|
||||||
|
hz: float
|
||||||
|
midi: float
|
||||||
|
note: str
|
||||||
|
conf: float
|
||||||
|
rms: float
|
||||||
|
|
||||||
|
|
||||||
|
NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
||||||
|
|
||||||
|
|
||||||
|
def hz_to_midi(hz: float) -> float:
|
||||||
|
if hz <= 0:
|
||||||
|
return float("nan")
|
||||||
|
return A4_MIDI + 12.0 * np.log2(hz / A4_HZ)
|
||||||
|
|
||||||
|
|
||||||
|
def midi_to_name(midi: float) -> str:
|
||||||
|
if not np.isfinite(midi):
|
||||||
|
return "--"
|
||||||
|
n = int(round(midi))
|
||||||
|
return f"{NOTE_NAMES[n % 12]}{n // 12 - 1}"
|
||||||
|
|
||||||
|
|
||||||
|
def list_devices() -> None:
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
for i, d in enumerate(sd.query_devices()):
|
||||||
|
name = str(d.get("name", "")).encode("utf-8", "replace").decode("utf-8", "replace")
|
||||||
|
print(
|
||||||
|
f"{i}: in={d['max_input_channels']} out={d['max_output_channels']} "
|
||||||
|
f"sr={d['default_samplerate']} {name}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
print("default device pair:", sd.default.device, flush=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def record(seconds: float, sr: int, device: int | None) -> np.ndarray:
|
||||||
|
frames = int(seconds * sr)
|
||||||
|
print(f"Recording {seconds:.1f}s @ {sr} Hz (Ctrl+C to abort)...", flush=True)
|
||||||
|
audio = sd.rec(frames, samplerate=sr, channels=1, dtype="float32", device=device)
|
||||||
|
sd.wait()
|
||||||
|
return audio[:, 0]
|
||||||
|
|
||||||
|
|
||||||
|
def save_wav(path: str, audio: np.ndarray, sr: int) -> None:
|
||||||
|
pcm = np.clip(audio, -1.0, 1.0)
|
||||||
|
pcm16 = (pcm * 32767.0).astype(np.int16)
|
||||||
|
with wave.open(path, "wb") as w:
|
||||||
|
w.setnchannels(1)
|
||||||
|
w.setsampwidth(2)
|
||||||
|
w.setframerate(sr)
|
||||||
|
w.writeframes(pcm16.tobytes())
|
||||||
|
|
||||||
|
|
||||||
|
def load_wav(path: str) -> tuple[np.ndarray, int]:
|
||||||
|
with wave.open(path, "rb") as w:
|
||||||
|
sr = w.getframerate()
|
||||||
|
nch = w.getnchannels()
|
||||||
|
raw = w.readframes(w.getnframes())
|
||||||
|
pcm = np.frombuffer(raw, dtype=np.int16).astype(np.float32) / 32768.0
|
||||||
|
if nch > 1:
|
||||||
|
pcm = pcm.reshape(-1, nch).mean(axis=1)
|
||||||
|
return pcm, sr
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_f0_acorr(
|
||||||
|
frame: np.ndarray,
|
||||||
|
sr: int,
|
||||||
|
fmin: float,
|
||||||
|
fmax: float,
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
"""Return (hz, confidence). confidence in [0,1] from normalized peak."""
|
||||||
|
x = frame.astype(np.float64)
|
||||||
|
x = x - np.mean(x)
|
||||||
|
rms = float(np.sqrt(np.mean(x * x)) + 1e-12)
|
||||||
|
if rms < 1e-4:
|
||||||
|
return 0.0, 0.0
|
||||||
|
|
||||||
|
# Hamming window
|
||||||
|
x = x * np.hamming(len(x))
|
||||||
|
corr = np.correlate(x, x, mode="full")
|
||||||
|
corr = corr[len(corr) // 2 :]
|
||||||
|
|
||||||
|
i_min = max(1, int(sr / fmax))
|
||||||
|
i_max = min(len(corr) - 1, int(sr / fmin))
|
||||||
|
if i_max <= i_min:
|
||||||
|
return 0.0, 0.0
|
||||||
|
|
||||||
|
seg = corr[i_min : i_max + 1]
|
||||||
|
peak_rel = int(np.argmax(seg))
|
||||||
|
peak = peak_rel + i_min
|
||||||
|
if corr[0] <= 1e-12:
|
||||||
|
return 0.0, 0.0
|
||||||
|
conf = float(corr[peak] / corr[0])
|
||||||
|
if conf < 0.25:
|
||||||
|
return 0.0, conf
|
||||||
|
|
||||||
|
# parabolic interpolation around peak
|
||||||
|
if 1 <= peak < len(corr) - 1:
|
||||||
|
a, b, c = corr[peak - 1], corr[peak], corr[peak + 1]
|
||||||
|
denom = a - 2 * b + c
|
||||||
|
if abs(denom) > 1e-12:
|
||||||
|
peak = peak + 0.5 * (a - c) / denom
|
||||||
|
|
||||||
|
hz = float(sr / peak)
|
||||||
|
if hz < fmin or hz > fmax:
|
||||||
|
return 0.0, conf
|
||||||
|
return hz, conf
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_audio(
|
||||||
|
audio: np.ndarray,
|
||||||
|
sr: int,
|
||||||
|
hop_ms: float = 50.0,
|
||||||
|
win_ms: float = 80.0,
|
||||||
|
fmin: float = 40.0,
|
||||||
|
fmax: float = 600.0,
|
||||||
|
conf_min: float = 0.35,
|
||||||
|
rms_min: float = 0.01,
|
||||||
|
) -> list[PitchFrame]:
|
||||||
|
hop = max(1, int(sr * hop_ms / 1000.0))
|
||||||
|
win = max(hop, int(sr * win_ms / 1000.0))
|
||||||
|
out: list[PitchFrame] = []
|
||||||
|
if len(audio) < win:
|
||||||
|
return out
|
||||||
|
|
||||||
|
for start in range(0, len(audio) - win, hop):
|
||||||
|
frame = audio[start : start + win]
|
||||||
|
rms = float(np.sqrt(np.mean(frame.astype(np.float64) ** 2)))
|
||||||
|
hz, conf = estimate_f0_acorr(frame, sr, fmin, fmax)
|
||||||
|
t = start / sr
|
||||||
|
if hz <= 0 or conf < conf_min or rms < rms_min:
|
||||||
|
out.append(PitchFrame(t, 0.0, float("nan"), "--", conf, rms))
|
||||||
|
continue
|
||||||
|
midi = hz_to_midi(hz)
|
||||||
|
out.append(PitchFrame(t, hz, midi, midi_to_name(midi), conf, rms))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def summarize(frames: list[PitchFrame], label: str = "") -> dict:
|
||||||
|
voiced = [f for f in frames if f.hz > 0 and np.isfinite(f.midi)]
|
||||||
|
if not voiced:
|
||||||
|
return {"label": label, "voiced": 0, "total": len(frames)}
|
||||||
|
|
||||||
|
midis = np.array([f.midi for f in voiced], dtype=np.float64)
|
||||||
|
hz = np.array([f.hz for f in voiced], dtype=np.float64)
|
||||||
|
notes = [f.note for f in voiced]
|
||||||
|
# round to nearest MIDI for histogram
|
||||||
|
rounded = [int(round(m)) for m in midis]
|
||||||
|
top = Counter(rounded).most_common(8)
|
||||||
|
# bass-ish: MIDI <= 48 (C3)
|
||||||
|
bass_ratio = float(np.mean(midis <= 48.0))
|
||||||
|
low_ratio = float(np.mean(midis <= 40.0)) # <= E2
|
||||||
|
return {
|
||||||
|
"label": label,
|
||||||
|
"voiced": len(voiced),
|
||||||
|
"total": len(frames),
|
||||||
|
"hz_median": float(np.median(hz)),
|
||||||
|
"hz_p10": float(np.percentile(hz, 10)),
|
||||||
|
"hz_p90": float(np.percentile(hz, 90)),
|
||||||
|
"midi_median": float(np.median(midis)),
|
||||||
|
"midi_p10": float(np.percentile(midis, 10)),
|
||||||
|
"midi_p90": float(np.percentile(midis, 90)),
|
||||||
|
"note_median": midi_to_name(float(np.median(midis))),
|
||||||
|
"top_notes": [(midi_to_name(float(n)), c) for n, c in top],
|
||||||
|
"bass_le_C3_ratio": bass_ratio,
|
||||||
|
"low_le_E2_ratio": low_ratio,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def print_summary(s: dict) -> None:
|
||||||
|
if s.get("voiced", 0) == 0:
|
||||||
|
print(f"[{s.get('label','')}] no pitched frames", flush=True)
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
f"[{s.get('label','')}] voiced={s['voiced']}/{s['total']} "
|
||||||
|
f"median={s['note_median']} ({s['midi_median']:.1f} / {s['hz_median']:.1f}Hz) "
|
||||||
|
f"p10={s['midi_p10']:.1f} p90={s['midi_p90']:.1f} "
|
||||||
|
f"<=E2={s['low_le_E2_ratio']*100:.0f}% <=C3={s['bass_le_C3_ratio']*100:.0f}%",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
tops = ", ".join(f"{n}×{c}" for n, c in s["top_notes"][:5])
|
||||||
|
print(f" top: {tops}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def write_frame_log(path: str, frames: list[PitchFrame], summary: dict) -> None:
|
||||||
|
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(f"# mic pitch {datetime.now().isoformat(timespec='seconds')}\n")
|
||||||
|
f.write("# " + json.dumps(summary, ensure_ascii=False) + "\n")
|
||||||
|
f.write("t_s,hz,midi,note,conf,rms\n")
|
||||||
|
for fr in frames:
|
||||||
|
midi = "" if not np.isfinite(fr.midi) else f"{fr.midi:.2f}"
|
||||||
|
f.write(
|
||||||
|
f"{fr.t:.3f},{fr.hz:.2f},{midi},{fr.note},{fr.conf:.3f},{fr.rms:.4f}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def live_monitor(
|
||||||
|
sr: int,
|
||||||
|
device: int | None,
|
||||||
|
fmin: float,
|
||||||
|
fmax: float,
|
||||||
|
seconds: float,
|
||||||
|
) -> None:
|
||||||
|
win = int(sr * 0.08)
|
||||||
|
hop = int(sr * 0.05)
|
||||||
|
print("Live pitch (Ctrl+C stop)...", flush=True)
|
||||||
|
buf = np.zeros(0, dtype=np.float32)
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
|
def callback(indata, frames, time_info, status): # noqa: ARG001
|
||||||
|
nonlocal buf
|
||||||
|
if status:
|
||||||
|
print(status, flush=True)
|
||||||
|
buf = np.concatenate([buf, indata[:, 0].copy()])
|
||||||
|
while len(buf) >= win:
|
||||||
|
frame = buf[:win]
|
||||||
|
buf = buf[hop:]
|
||||||
|
hz, conf = estimate_f0_acorr(frame, sr, fmin, fmax)
|
||||||
|
rms = float(np.sqrt(np.mean(frame.astype(np.float64) ** 2)))
|
||||||
|
if hz > 0 and conf >= 0.35 and rms >= 0.01:
|
||||||
|
midi = hz_to_midi(hz)
|
||||||
|
print(
|
||||||
|
f"{time.time()-t0:6.1f}s {midi_to_name(midi):4s} "
|
||||||
|
f"midi={midi:5.1f} {hz:6.1f}Hz conf={conf:.2f} rms={rms:.3f}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with sd.InputStream(samplerate=sr, channels=1, dtype="float32", device=device, callback=callback):
|
||||||
|
if seconds > 0:
|
||||||
|
sd.sleep(int(seconds * 1000))
|
||||||
|
else:
|
||||||
|
while True:
|
||||||
|
sd.sleep(200)
|
||||||
|
|
||||||
|
|
||||||
|
def compare_summaries(a: dict, b: dict) -> None:
|
||||||
|
print("\n======== COMPARE ========", flush=True)
|
||||||
|
print_summary(a)
|
||||||
|
print_summary(b)
|
||||||
|
if a.get("voiced", 0) == 0 or b.get("voiced", 0) == 0:
|
||||||
|
print("Need pitched content in both takes.", flush=True)
|
||||||
|
return
|
||||||
|
d_midi = b["midi_median"] - a["midi_median"]
|
||||||
|
print(f"median delta (B-A): {d_midi:+.2f} semitones", flush=True)
|
||||||
|
if d_midi <= -9:
|
||||||
|
print("PASS-ish: B is about an octave lower than A (expected III+ vs I/II).", flush=True)
|
||||||
|
elif d_midi <= -5:
|
||||||
|
print("PARTIAL: B lower than A but less than full octave.", flush=True)
|
||||||
|
elif abs(d_midi) < 2:
|
||||||
|
print("FAIL-ish: medians similar — register fix may not be audible on mic mix.", flush=True)
|
||||||
|
else:
|
||||||
|
print("CHECK: unexpected direction/amount; inspect top notes / low_le_E2 ratios.", flush=True)
|
||||||
|
print(
|
||||||
|
f"low<=E2: A={a['low_le_E2_ratio']*100:.0f}% B={b['low_le_E2_ratio']*100:.0f}%",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
p = argparse.ArgumentParser(description="Mic pitch analyzer for K1 register tests")
|
||||||
|
p.add_argument("--list", action="store_true", help="List audio devices")
|
||||||
|
p.add_argument("--device", type=int, default=None, help="Input device index")
|
||||||
|
p.add_argument("--sr", type=int, default=16000)
|
||||||
|
p.add_argument("--seconds", type=float, default=8.0)
|
||||||
|
p.add_argument("--live", action="store_true")
|
||||||
|
p.add_argument("--wav", default="", help="Analyze existing wav instead of recording")
|
||||||
|
p.add_argument("--out", default="", help="Prefix for wav/csv outputs")
|
||||||
|
p.add_argument("--fmin", type=float, default=40.0, help="Min F0 Hz (bass ~41=E1)")
|
||||||
|
p.add_argument("--fmax", type=float, default=600.0, help="Max F0 Hz")
|
||||||
|
p.add_argument(
|
||||||
|
"--compare",
|
||||||
|
nargs=2,
|
||||||
|
metavar=("A", "B"),
|
||||||
|
help="Compare two wav/csv summary sources (wav preferred)",
|
||||||
|
)
|
||||||
|
p.add_argument("--label", default="")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
if args.list:
|
||||||
|
list_devices()
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.compare:
|
||||||
|
summaries = []
|
||||||
|
for i, path in enumerate(args.compare):
|
||||||
|
label = "A" if i == 0 else "B"
|
||||||
|
if path.lower().endswith(".wav"):
|
||||||
|
audio, sr = load_wav(path)
|
||||||
|
frames = analyze_audio(audio, sr, fmin=args.fmin, fmax=args.fmax)
|
||||||
|
s = summarize(frames, label=f"{label}:{os.path.basename(path)}")
|
||||||
|
else:
|
||||||
|
raise SystemExit("compare expects .wav files")
|
||||||
|
summaries.append(s)
|
||||||
|
compare_summaries(summaries[0], summaries[1])
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.live:
|
||||||
|
live_monitor(args.sr, args.device, args.fmin, args.fmax, args.seconds if args.seconds > 0 else 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
prefix = args.out or f"mic_pitch_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||||
|
if args.wav:
|
||||||
|
audio, sr = load_wav(args.wav)
|
||||||
|
label = args.label or os.path.basename(args.wav)
|
||||||
|
else:
|
||||||
|
audio = record(args.seconds, args.sr, args.device)
|
||||||
|
sr = args.sr
|
||||||
|
label = args.label or "rec"
|
||||||
|
wav_path = prefix if prefix.lower().endswith(".wav") else prefix + ".wav"
|
||||||
|
save_wav(wav_path, audio, sr)
|
||||||
|
print(f"Saved wav: {os.path.abspath(wav_path)}", flush=True)
|
||||||
|
|
||||||
|
frames = analyze_audio(audio, sr, fmin=args.fmin, fmax=args.fmax)
|
||||||
|
summary = summarize(frames, label=label)
|
||||||
|
print_summary(summary)
|
||||||
|
log_path = prefix if prefix.lower().endswith(".csv") else prefix + ".csv"
|
||||||
|
# if prefix was .wav, still write .csv alongside
|
||||||
|
if log_path.lower().endswith(".wav.csv"):
|
||||||
|
log_path = log_path[:-8] + ".csv"
|
||||||
|
elif prefix.lower().endswith(".wav"):
|
||||||
|
log_path = prefix[:-4] + ".csv"
|
||||||
|
write_frame_log(log_path, frames, summary)
|
||||||
|
print(f"Saved log: {os.path.abspath(log_path)}", flush=True)
|
||||||
|
print(
|
||||||
|
"Tip: record I/II then III+ separately, then:\n"
|
||||||
|
" python tools/mic_pitch_analyze.py --compare mic_i.wav mic_iii.wav",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nStopped.", flush=True)
|
||||||
|
sys.exit(130)
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Pack external W25Q128 tone/logo image for K1 (0903).
|
"""Pack external W25Q128 tone/logo image for K1 (0916_bank).
|
||||||
|
|
||||||
Layout (absolute W25Q128 offsets):
|
Layout (absolute W25Q128 offsets):
|
||||||
0x00000000 logo.bin (legacy pad; boot UI uses UI0902 full-screen logo)
|
0x00000000 logo.bin (legacy pad; boot UI uses UI0902 full-screen logo)
|
||||||
0x0000CB70 Charg.bin (legacy pad; charge UI uses UI0902_CHARGE_SCREEN full-screen)
|
0x0000CB70 Charg.bin (legacy pad; charge UI uses UI0902_CHARGE_SCREEN)
|
||||||
0x0001B8F0 1.bin 普通/专业 31 rhythms
|
0x0001B8F0 1.bin 普通/专业 31 rhythms
|
||||||
0x0009D07D 2.bin 海阔天空
|
0x0009D07D 2.bin 本地曲目(变长,<=41KB)
|
||||||
0x000A8607 3.bin 万能模式
|
after 2.bin 3.bin 万能模式(随 2.bin 长度后移;写入 ExtFlash_Tone_Addr.h)
|
||||||
|
|
||||||
Outputs:
|
Outputs:
|
||||||
tools/out/extflash_tone_0903.bin
|
tools/out/extflash_tone_0916.bin / .res
|
||||||
|
tools/out/extflash_ALL_tone0916_ui0902.res
|
||||||
project/inc/ExtFlash_Tone_Addr.h
|
project/inc/ExtFlash_Tone_Addr.h
|
||||||
Doc/音色文件/0903/FLASH_MAP.txt
|
Doc/音色文件/0916_bank/FLASH_MAP.txt
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -24,11 +25,27 @@ from pathlib import Path
|
||||||
ROOT = Path(__file__).resolve().parents[1] # firmware project root
|
ROOT = Path(__file__).resolve().parents[1] # firmware project root
|
||||||
REPO = ROOT.parent.parent # 一诺国际吉他
|
REPO = ROOT.parent.parent # 一诺国际吉他
|
||||||
OUT_DIR = ROOT / "tools" / "out"
|
OUT_DIR = ROOT / "tools" / "out"
|
||||||
OUT_BIN = OUT_DIR / "extflash_tone_0903.bin"
|
TONE_TAG = "0916"
|
||||||
|
OUT_BIN = OUT_DIR / f"extflash_tone_{TONE_TAG}.bin"
|
||||||
|
OUT_RES = OUT_DIR / f"extflash_tone_{TONE_TAG}.res"
|
||||||
|
OUT_ALL_RES = OUT_DIR / f"extflash_ALL_tone{TONE_TAG}_ui0902.res"
|
||||||
|
# Keep legacy alias names for older tools that still look for 0903
|
||||||
|
OUT_BIN_LEGACY = OUT_DIR / "extflash_tone_0903.bin"
|
||||||
|
OUT_RES_LEGACY = OUT_DIR / "extflash_tone_0903.res"
|
||||||
|
OUT_ALL_LEGACY = OUT_DIR / "extflash_ALL_tone0903_ui0902.res"
|
||||||
OUT_HDR = ROOT / "project" / "inc" / "ExtFlash_Tone_Addr.h"
|
OUT_HDR = ROOT / "project" / "inc" / "ExtFlash_Tone_Addr.h"
|
||||||
OUT_MAP = REPO / "Doc" / "音色文件" / "0903" / "FLASH_MAP.txt"
|
TONE_DIR = REPO / "Doc" / "音色文件" / "0916_bank"
|
||||||
|
OUT_MAP = TONE_DIR / "FLASH_MAP.txt"
|
||||||
|
UI0902_BIN = OUT_DIR / "ui0902_res.bin"
|
||||||
|
|
||||||
TONE_DIR = REPO / "Doc" / "音色文件" / "0903"
|
OFF_LOGO = 0x00000000
|
||||||
|
OFF_CHARGING = 0x0000CB70
|
||||||
|
OFF_BIN1 = 0x0001B8F0
|
||||||
|
OFF_BIN2 = 0x0009D07D
|
||||||
|
# BIN3 placed immediately after 2.bin (computed at pack time)
|
||||||
|
UI0902_RES_BASE = 0x00100000
|
||||||
|
DAB_MAGIC = b"\xABDAB"
|
||||||
|
MAX_SONG_BIN_BYTES = 41 * 1024 # 曲目文件(2.bin)硬上限 41KB
|
||||||
|
|
||||||
|
|
||||||
def find_ziliao() -> Path:
|
def find_ziliao() -> Path:
|
||||||
|
|
@ -38,29 +55,69 @@ def find_ziliao() -> Path:
|
||||||
raise FileNotFoundError("资料/logo.bin + Charg.bin not found under repo")
|
raise FileNotFoundError("资料/logo.bin + Charg.bin not found under repo")
|
||||||
|
|
||||||
|
|
||||||
|
def dab_ok(blob: bytes, off: int, expect_cnt: int | None = None) -> None:
|
||||||
|
if off + 16 > len(blob):
|
||||||
|
raise SystemExit(f"DAB check @0x{off:X}: past end of pack ({len(blob)})")
|
||||||
|
magic = blob[off : off + 4]
|
||||||
|
cnt = struct.unpack_from("<I", blob, off + 12)[0]
|
||||||
|
if magic != DAB_MAGIC:
|
||||||
|
raise SystemExit(
|
||||||
|
f"DAB check @0x{off:X}: bad magic {magic.hex()} (want {DAB_MAGIC.hex()})"
|
||||||
|
)
|
||||||
|
if expect_cnt is not None and cnt != expect_cnt:
|
||||||
|
raise SystemExit(f"DAB check @0x{off:X}: cnt={cnt} want {expect_cnt}")
|
||||||
|
|
||||||
|
|
||||||
|
def require_region_equals(blob: bytes, off: int, src: bytes, label: str) -> None:
|
||||||
|
end = off + len(src)
|
||||||
|
if end > len(blob):
|
||||||
|
raise SystemExit(f"{label}: pack too short for region @0x{off:X}+{len(src)}")
|
||||||
|
if blob[off:end] != src:
|
||||||
|
raise SystemExit(f"{label}: bytes @0x{off:X} do not match source file")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
if not TONE_DIR.is_dir():
|
||||||
|
raise SystemExit(f"missing tone dir: {TONE_DIR}")
|
||||||
|
|
||||||
ziliao = find_ziliao()
|
ziliao = find_ziliao()
|
||||||
|
bin1_path = TONE_DIR / "1.bin"
|
||||||
|
bin2_path = TONE_DIR / "2.bin"
|
||||||
|
bin3_path = TONE_DIR / "3.bin"
|
||||||
|
for p in (bin1_path, bin2_path, bin3_path):
|
||||||
|
if not p.is_file():
|
||||||
|
raise SystemExit(f"missing {p}")
|
||||||
|
|
||||||
|
bin2_data = bin2_path.read_bytes()
|
||||||
|
if len(bin2_data) > MAX_SONG_BIN_BYTES:
|
||||||
|
raise SystemExit(
|
||||||
|
f"2.bin (曲目) too large: {len(bin2_data)} bytes > {MAX_SONG_BIN_BYTES} (41KB)"
|
||||||
|
)
|
||||||
|
|
||||||
|
off_bin3 = OFF_BIN2 + len(bin2_data)
|
||||||
parts = [
|
parts = [
|
||||||
("LOGO", ziliao / "logo.bin", 0x00000000, "legacy pad; boot uses UI0902_BOOT_LOGO"),
|
("LOGO", ziliao / "logo.bin", OFF_LOGO, "legacy pad; boot uses UI0902_BOOT_LOGO"),
|
||||||
("CHARGING", ziliao / "Charg.bin", 0x0000CB70, "legacy pad; UI uses UI0902_CHARGE_SCREEN"),
|
("CHARGING", ziliao / "Charg.bin", OFF_CHARGING, "legacy pad; UI uses UI0902_CHARGE_SCREEN"),
|
||||||
("BIN1_RHYTHM", TONE_DIR / "1.bin", 0x0001B8F0, "普通/专业 31 条节奏"),
|
("BIN1_RHYTHM", bin1_path, OFF_BIN1, "普通/专业 31 条节奏"),
|
||||||
("BIN2_SONG_HAITIAN", TONE_DIR / "2.bin", None, "本地曲目 海阔天空"),
|
("BIN2_SONG_HAITIAN", bin2_path, OFF_BIN2, "本地曲目"),
|
||||||
("BIN3_UNIVERSAL", TONE_DIR / "3.bin", None, "万能模式"),
|
("BIN3_UNIVERSAL", bin3_path, off_bin3, "万能模式(紧跟 2.bin 之后)"),
|
||||||
]
|
]
|
||||||
|
|
||||||
blobs = []
|
blobs: list[bytes] = []
|
||||||
cursor = 0
|
cursor = 0
|
||||||
rows = []
|
rows: list[tuple[str, int, int, str]] = []
|
||||||
for name, path, force_off, note in parts:
|
for name, path, force_off, note in parts:
|
||||||
data = path.read_bytes()
|
data = path.read_bytes()
|
||||||
if force_off is not None:
|
if cursor > force_off:
|
||||||
if cursor > force_off:
|
raise SystemExit(
|
||||||
raise SystemExit(f"{name}: cursor 0x{cursor:X} past forced 0x{force_off:X}")
|
f"{name}: cursor 0x{cursor:X} past forced 0x{force_off:X} "
|
||||||
if cursor < force_off:
|
f"(previous blob too large)"
|
||||||
pad = force_off - cursor
|
)
|
||||||
blobs.append(b"\xFF" * pad)
|
if cursor < force_off:
|
||||||
cursor = force_off
|
pad = force_off - cursor
|
||||||
rows.append((f"(pad)", force_off - pad, pad, "gap fill 0xFF"))
|
blobs.append(b"\xFF" * pad)
|
||||||
|
rows.append(("(pad)", cursor, pad, "gap fill 0xFF"))
|
||||||
|
cursor = force_off
|
||||||
off = cursor
|
off = cursor
|
||||||
blobs.append(data)
|
blobs.append(data)
|
||||||
cursor += len(data)
|
cursor += len(data)
|
||||||
|
|
@ -69,9 +126,33 @@ def main() -> None:
|
||||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
packed = b"".join(blobs)
|
packed = b"".join(blobs)
|
||||||
OUT_BIN.write_bytes(packed)
|
OUT_BIN.write_bytes(packed)
|
||||||
|
OUT_RES.write_bytes(packed)
|
||||||
|
OUT_BIN_LEGACY.write_bytes(packed)
|
||||||
|
OUT_RES_LEGACY.write_bytes(packed)
|
||||||
|
|
||||||
# named lookup
|
|
||||||
by_name = {r[0]: r for r in rows if not r[0].startswith("(")}
|
by_name = {r[0]: r for r in rows if not r[0].startswith("(")}
|
||||||
|
bin1_data = bin1_path.read_bytes()
|
||||||
|
bin3_data = bin3_path.read_bytes()
|
||||||
|
|
||||||
|
dab_ok(packed, OFF_BIN1, expect_cnt=31)
|
||||||
|
dab_ok(packed, OFF_BIN2, expect_cnt=1)
|
||||||
|
dab_ok(packed, off_bin3, expect_cnt=3)
|
||||||
|
if by_name["BIN1_RHYTHM"][1] != OFF_BIN1:
|
||||||
|
raise SystemExit(f"BIN1 placed @0x{by_name['BIN1_RHYTHM'][1]:X} want 0x{OFF_BIN1:X}")
|
||||||
|
if by_name["BIN2_SONG_HAITIAN"][1] != OFF_BIN2:
|
||||||
|
raise SystemExit(f"BIN2 placed @0x{by_name['BIN2_SONG_HAITIAN'][1]:X} want 0x{OFF_BIN2:X}")
|
||||||
|
if by_name["BIN3_UNIVERSAL"][1] != off_bin3:
|
||||||
|
raise SystemExit(f"BIN3 placed @0x{by_name['BIN3_UNIVERSAL'][1]:X} want 0x{off_bin3:X}")
|
||||||
|
require_region_equals(packed, OFF_LOGO, (ziliao / "logo.bin").read_bytes(), "LOGO")
|
||||||
|
require_region_equals(packed, OFF_CHARGING, (ziliao / "Charg.bin").read_bytes(), "CHARGING")
|
||||||
|
require_region_equals(packed, OFF_BIN1, bin1_data, "BIN1/1.bin")
|
||||||
|
require_region_equals(packed, OFF_BIN2, bin2_data, "BIN2/2.bin")
|
||||||
|
require_region_equals(packed, off_bin3, bin3_data, "BIN3/3.bin")
|
||||||
|
|
||||||
|
if cursor > UI0902_RES_BASE:
|
||||||
|
raise SystemExit(
|
||||||
|
f"ERROR: tone pack end 0x{cursor:X} overflows UI0902 @ 0x{UI0902_RES_BASE:X}"
|
||||||
|
)
|
||||||
|
|
||||||
hdr = f"""#ifndef __EXTFLASH_TONE_ADDR_H
|
hdr = f"""#ifndef __EXTFLASH_TONE_ADDR_H
|
||||||
#define __EXTFLASH_TONE_ADDR_H
|
#define __EXTFLASH_TONE_ADDR_H
|
||||||
|
|
@ -79,10 +160,10 @@ def main() -> None:
|
||||||
/* Auto-generated by tools/pack_extflash_tone_0903.py — do not hand-edit. */
|
/* Auto-generated by tools/pack_extflash_tone_0903.py — do not hand-edit. */
|
||||||
/* W25Q128 absolute offsets. Boot logo display uses UI0902_BOOT_LOGO_ADDR. */
|
/* W25Q128 absolute offsets. Boot logo display uses UI0902_BOOT_LOGO_ADDR. */
|
||||||
/* Tone resource pack has no in-bin version field; release tag = EXTFLASH_TONE_RES_VER. */
|
/* Tone resource pack has no in-bin version field; release tag = EXTFLASH_TONE_RES_VER. */
|
||||||
#define EXTFLASH_TONE_RES_VER "0903"
|
#define EXTFLASH_TONE_RES_VER "{TONE_TAG}"
|
||||||
#define EXTFLASH_TONE_RES_VER_MAJOR 0
|
#define EXTFLASH_TONE_RES_VER_MAJOR 0
|
||||||
#define EXTFLASH_TONE_RES_VER_MINOR 9
|
#define EXTFLASH_TONE_RES_VER_MINOR 9
|
||||||
#define EXTFLASH_TONE_RES_VER_PATCH 3
|
#define EXTFLASH_TONE_RES_VER_PATCH 16
|
||||||
|
|
||||||
#define EXTFLASH_LOGO_ADDR 0x{by_name['LOGO'][1]:08X}UL
|
#define EXTFLASH_LOGO_ADDR 0x{by_name['LOGO'][1]:08X}UL
|
||||||
#define EXTFLASH_LOGO_SIZE {by_name['LOGO'][2]}UL
|
#define EXTFLASH_LOGO_SIZE {by_name['LOGO'][2]}UL
|
||||||
|
|
@ -92,13 +173,13 @@ def main() -> None:
|
||||||
#define EXTFLASH_CHARGING_W 160
|
#define EXTFLASH_CHARGING_W 160
|
||||||
#define EXTFLASH_CHARGING_H 190
|
#define EXTFLASH_CHARGING_H 190
|
||||||
|
|
||||||
#define EXTFLASH_BIN1_RHYTHM_ADDR 0x{by_name['BIN1_RHYTHM'][1]:08X}UL
|
#define EXTFLASH_BIN1_RHYTHM_ADDR 0x{OFF_BIN1:08X}UL
|
||||||
#define EXTFLASH_BIN1_RHYTHM_SIZE {by_name['BIN1_RHYTHM'][2]}UL
|
#define EXTFLASH_BIN1_RHYTHM_SIZE {by_name['BIN1_RHYTHM'][2]}UL
|
||||||
|
|
||||||
#define EXTFLASH_BIN2_SONG_HAITIAN_ADDR 0x{by_name['BIN2_SONG_HAITIAN'][1]:08X}UL
|
#define EXTFLASH_BIN2_SONG_HAITIAN_ADDR 0x{OFF_BIN2:08X}UL
|
||||||
#define EXTFLASH_BIN2_SONG_HAITIAN_SIZE {by_name['BIN2_SONG_HAITIAN'][2]}UL
|
#define EXTFLASH_BIN2_SONG_HAITIAN_SIZE {by_name['BIN2_SONG_HAITIAN'][2]}UL
|
||||||
|
|
||||||
#define EXTFLASH_BIN3_UNIVERSAL_ADDR 0x{by_name['BIN3_UNIVERSAL'][1]:08X}UL
|
#define EXTFLASH_BIN3_UNIVERSAL_ADDR 0x{off_bin3:08X}UL
|
||||||
#define EXTFLASH_BIN3_UNIVERSAL_SIZE {by_name['BIN3_UNIVERSAL'][2]}UL
|
#define EXTFLASH_BIN3_UNIVERSAL_SIZE {by_name['BIN3_UNIVERSAL'][2]}UL
|
||||||
|
|
||||||
/* Convenience aliases used by UI ADDRESS */
|
/* Convenience aliases used by UI ADDRESS */
|
||||||
|
|
@ -107,7 +188,7 @@ def main() -> None:
|
||||||
#define FLASH_ADDR_SONG_HAITIAN EXTFLASH_BIN2_SONG_HAITIAN_ADDR
|
#define FLASH_ADDR_SONG_HAITIAN EXTFLASH_BIN2_SONG_HAITIAN_ADDR
|
||||||
#define FLASH_ADDR_MODE_UNIVERSAL EXTFLASH_BIN3_UNIVERSAL_ADDR /* 万能 */
|
#define FLASH_ADDR_MODE_UNIVERSAL EXTFLASH_BIN3_UNIVERSAL_ADDR /* 万能 */
|
||||||
|
|
||||||
/* Legacy AutoBand bank — not repacked in 0903; left unchanged in firmware */
|
/* Legacy AutoBand bank — not repacked; may overlap 2.bin — do not enable until remapped */
|
||||||
#define FLASH_ADDR_AUTOBAND_LEGACY 0x0009EB5FUL
|
#define FLASH_ADDR_AUTOBAND_LEGACY 0x0009EB5FUL
|
||||||
|
|
||||||
#define EXTFLASH_TONE_PACK_END 0x{cursor:08X}UL
|
#define EXTFLASH_TONE_PACK_END 0x{cursor:08X}UL
|
||||||
|
|
@ -119,9 +200,13 @@ def main() -> None:
|
||||||
OUT_HDR.write_text(hdr, encoding="utf-8", newline="\n")
|
OUT_HDR.write_text(hdr, encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
map_lines = [
|
map_lines = [
|
||||||
"K1 external Flash map — tone pack 0903",
|
f"K1 external Flash map — tone pack {TONE_TAG}",
|
||||||
f"Packed file: Code/.../tools/out/{OUT_BIN.name} ({len(packed)} bytes)",
|
f"Packed file: Code/.../tools/out/{OUT_BIN.name} ({len(packed)} bytes)",
|
||||||
f"Ends at 0x{cursor:X}; UI0902_RES_BASE=0x00100000; free gap={0x100000 - cursor} bytes",
|
f"Also: {OUT_RES.name}; ALL.res when ui0902_res.bin present",
|
||||||
|
f"Ends at 0x{cursor:X}; UI0902_RES_BASE=0x00100000; free gap={UI0902_RES_BASE - cursor} bytes",
|
||||||
|
"",
|
||||||
|
f"NOTE: BIN3 follows 2.bin @ 0x{off_bin3:X} (2.bin grew past old fixed 0xA71AC).",
|
||||||
|
" 2.bin must be <= 41KB; LOGO/CHARG/BIN1/BIN2 offsets remain fixed.",
|
||||||
"",
|
"",
|
||||||
f"{'Name':<22} {'Offset':>10} {'Size':>10} Note",
|
f"{'Name':<22} {'Offset':>10} {'Size':>10} Note",
|
||||||
"-" * 72,
|
"-" * 72,
|
||||||
|
|
@ -131,32 +216,62 @@ def main() -> None:
|
||||||
map_lines += [
|
map_lines += [
|
||||||
"",
|
"",
|
||||||
"Firmware ADDRESS mapping:",
|
"Firmware ADDRESS mapping:",
|
||||||
" 普通/专业 -> FLASH_ADDR_MODE_NORMAL/EXPERT (1.bin)",
|
" 普通/专业 -> FLASH_ADDR_MODE_NORMAL/EXPERT (1.bin @ 0x1B8F0)",
|
||||||
" 海阔天空 -> FLASH_ADDR_SONG_HAITIAN (2.bin)",
|
" 本地曲目 -> FLASH_ADDR_SONG_HAITIAN (2.bin @ 0x9D07D, max 41KB)",
|
||||||
" 万能 -> FLASH_ADDR_MODE_UNIVERSAL (3.bin)",
|
f" 万能 -> FLASH_ADDR_MODE_UNIVERSAL (3.bin @ 0x{off_bin3:X})",
|
||||||
" AutoBand -> FLASH_ADDR_AUTOBAND_LEGACY 0x9EB5F (unchanged; overlaps 2.bin region — do not enable until remapped)",
|
|
||||||
" Boot logo -> UI0902_BOOT_LOGO_ADDR (full-screen); packed logo.bin only pads 0x0..0xCB70",
|
" Boot logo -> UI0902_BOOT_LOGO_ADDR (full-screen); packed logo.bin only pads 0x0..0xCB70",
|
||||||
" Charging -> EXTFLASH_CHARGING_ADDR",
|
" Charging -> EXTFLASH_CHARGING_ADDR (legacy pad) / UI0902_CHARGE_SCREEN",
|
||||||
"",
|
"",
|
||||||
"Add a local song:",
|
"Rebuild after pack:",
|
||||||
" 1. Pack the new preset into 2.bin",
|
" 1. python tools/pack_extflash_tone_0903.py",
|
||||||
" 2. Append a row to local_songs.csv (index,code,name)",
|
" 2. Rebuild MCU (ExtFlash_Tone_Addr.h updated)",
|
||||||
" 3. python tools/pack_extflash_tone_0903.py (also regenerates LocalSongNames.h)",
|
" 3. Flash MCU + ExtFlash ALL.res",
|
||||||
" 4. Rebuild firmware and flash MCU + ExtFlash",
|
|
||||||
]
|
]
|
||||||
|
OUT_MAP.parent.mkdir(parents=True, exist_ok=True)
|
||||||
OUT_MAP.write_text("\n".join(map_lines) + "\n", encoding="utf-8", newline="\n")
|
OUT_MAP.write_text("\n".join(map_lines) + "\n", encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
print(f"Wrote {OUT_BIN} ({len(packed)} bytes)")
|
print(f"Wrote {OUT_BIN} ({len(packed)} bytes)")
|
||||||
|
print(f"Wrote {OUT_RES} ({len(packed)} bytes)")
|
||||||
print(f"Wrote {OUT_HDR}")
|
print(f"Wrote {OUT_HDR}")
|
||||||
print(f"Wrote {OUT_MAP}")
|
print(f"Wrote {OUT_MAP}")
|
||||||
for name, off, size, note in rows:
|
for name, off, size, note in rows:
|
||||||
print(f" 0x{off:08X} {size:8d} {name} {note}")
|
print(f" 0x{off:08X} {size:8d} {name} {note}")
|
||||||
if cursor > 0x00100000:
|
print(f"OK: {UI0902_RES_BASE - cursor} bytes free before UI0902 @ 0x{UI0902_RES_BASE:X}")
|
||||||
raise SystemExit("ERROR: pack overflows into UI0902_RES_BASE")
|
print("OK: LOGO/CHARG/BIN1/BIN2/BIN3 present and match source files")
|
||||||
print(f"OK: {0x00100000 - cursor} bytes free before UI0902 @ 0x00100000")
|
print(f"OK: 2.bin size {len(bin2_data)} <= 41KB; BIN3 @ 0x{off_bin3:X}")
|
||||||
|
|
||||||
# Keep LocalSongNames.h in sync with Doc/.../local_songs.csv when packing tones.
|
if UI0902_BIN.is_file():
|
||||||
|
ui = UI0902_BIN.read_bytes()
|
||||||
|
all_res = packed + (b"\xFF" * (UI0902_RES_BASE - len(packed))) + ui
|
||||||
|
require_region_equals(all_res, OFF_BIN1, bin1_data, "ALL.res BIN1")
|
||||||
|
require_region_equals(all_res, OFF_BIN2, bin2_data, "ALL.res BIN2")
|
||||||
|
require_region_equals(all_res, off_bin3, bin3_data, "ALL.res BIN3")
|
||||||
|
dab_ok(all_res, OFF_BIN1, 31)
|
||||||
|
dab_ok(all_res, OFF_BIN2, 1)
|
||||||
|
dab_ok(all_res, off_bin3, 3)
|
||||||
|
# UI0902 magic/presence: non-empty and starts within expected region
|
||||||
|
if len(ui) < 1024:
|
||||||
|
raise SystemExit(f"UI0902 pack suspiciously small: {len(ui)}")
|
||||||
|
OUT_ALL_RES.write_bytes(all_res)
|
||||||
|
OUT_ALL_LEGACY.write_bytes(all_res)
|
||||||
|
repo_out = REPO / "tools" / "out"
|
||||||
|
repo_out.mkdir(parents=True, exist_ok=True)
|
||||||
|
(repo_out / OUT_ALL_RES.name).write_bytes(all_res)
|
||||||
|
(repo_out / OUT_ALL_LEGACY.name).write_bytes(all_res)
|
||||||
|
(repo_out / OUT_RES.name).write_bytes(packed)
|
||||||
|
(repo_out / OUT_RES_LEGACY.name).write_bytes(packed)
|
||||||
|
print(f"Wrote {OUT_ALL_RES} ({len(all_res)} bytes) — verified logo/charg/1/2/3 + UI0902")
|
||||||
|
else:
|
||||||
|
print(f"WARN: {UI0902_BIN} missing; skipped ALL.res")
|
||||||
|
|
||||||
|
# Prefer 0914 CSV; fall back to 0903
|
||||||
|
csv_0914 = TONE_DIR / "local_songs.csv"
|
||||||
gen = ROOT / "tools" / "gen_local_song_names.py"
|
gen = ROOT / "tools" / "gen_local_song_names.py"
|
||||||
|
if not csv_0914.is_file():
|
||||||
|
src = REPO / "Doc" / "音色文件" / "0903" / "local_songs.csv"
|
||||||
|
if src.is_file():
|
||||||
|
csv_0914.write_bytes(src.read_bytes())
|
||||||
|
print(f"Copied {src.name} -> {csv_0914}")
|
||||||
r = subprocess.run([sys.executable, str(gen)], check=False)
|
r = subprocess.run([sys.executable, str(gen)], check=False)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
raise SystemExit(f"gen_local_song_names.py failed ({r.returncode})")
|
raise SystemExit(f"gen_local_song_names.py failed ({r.returncode})")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Append missing GBK glyph 闭 (0xB1,0xD5) to all CHS* tables + Chinese_font_* arrays.
|
||||||
|
|
||||||
|
Settings autoclose label uses 「关闭」; 关 exists, 闭 was missing → blank second char.
|
||||||
|
Uses tools/gen_wqy_font.py rasterizer (WenQuanYi BDF).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, os.path.join(ROOT, "tools"))
|
||||||
|
import gen_wqy_font as wqy # noqa: E402
|
||||||
|
|
||||||
|
HEADER = os.path.join(ROOT, "device", "LCD_ILI9341", "Drv_ILI9341_Lcd_ChineseFont.h")
|
||||||
|
|
||||||
|
# size -> (table name, font array name, bytes per glyph)
|
||||||
|
SIZES = {
|
||||||
|
8: ("CHS8Table", "Chinese_font_8", 8),
|
||||||
|
9: ("CHS9Table", "Chinese_font_9", 18),
|
||||||
|
11: ("CHS11Table", "Chinese_font_11", 22),
|
||||||
|
12: ("CHS12Table", "Chinese_font_12", 24),
|
||||||
|
13: ("CHS13Table", "Chinese_font_13", 26),
|
||||||
|
15: ("CHS15Table", "Chinese_font_15", 30),
|
||||||
|
16: ("CHS16Table", "Chinese_font_16", 32),
|
||||||
|
24: ("CHS24Table", "Chinese_font_24", 72),
|
||||||
|
}
|
||||||
|
|
||||||
|
BI_H, BI_L = 0xB1, 0xD5
|
||||||
|
NEEDLE = "{0x%02X,0x%02X}" % (BI_H, BI_L)
|
||||||
|
|
||||||
|
|
||||||
|
def render_bi(size: int) -> list[int]:
|
||||||
|
"""Rasterize 闭; size 24 has no WQY map — nearest-neighbor upscale from 16."""
|
||||||
|
src = 16 if size == 24 else size
|
||||||
|
pixels = wqy.glyph_pixels("闭", src, latin=False)
|
||||||
|
if size == 24:
|
||||||
|
# 16x16 -> 24x24
|
||||||
|
up = []
|
||||||
|
for y in range(24):
|
||||||
|
sy = min(15, y * 16 // 24)
|
||||||
|
for x in range(24):
|
||||||
|
sx = min(15, x * 16 // 24)
|
||||||
|
up.append(pixels[sy * 16 + sx])
|
||||||
|
pixels = up
|
||||||
|
return wqy.cn_to_bytes_msb(pixels, size, size)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
text = open(HEADER, encoding="utf-8", errors="replace").read()
|
||||||
|
for size, (tname, aname, nbytes) in SIZES.items():
|
||||||
|
tm = re.search(
|
||||||
|
rf"(const char {tname}\[\]\[2\] = \{{)(.*?)(\n\}};)", text, re.S
|
||||||
|
)
|
||||||
|
if not tm:
|
||||||
|
raise SystemExit(f"missing {tname}")
|
||||||
|
body = tm.group(2)
|
||||||
|
compact = re.sub(r"\s+", "", body).lower()
|
||||||
|
if NEEDLE.lower() in compact:
|
||||||
|
print(f"skip table {tname}: already has 闭")
|
||||||
|
continue
|
||||||
|
|
||||||
|
body = body.rstrip() + f"\n {{0x{BI_H:02X},0x{BI_L:02X}}}, // 闭\n"
|
||||||
|
text = text[: tm.start()] + tm.group(1) + body + tm.group(3) + text[tm.end() :]
|
||||||
|
print(f"append table {tname}")
|
||||||
|
|
||||||
|
am = re.search(
|
||||||
|
rf"(const unsigned char {aname}\[\]\[\d+\] = \{{)(.*?)(\n\}};)", text, re.S
|
||||||
|
)
|
||||||
|
if not am:
|
||||||
|
raise SystemExit(f"missing {aname}")
|
||||||
|
fbody = am.group(2)
|
||||||
|
idx = len(re.findall(r'/\*"', fbody))
|
||||||
|
data = render_bi(size)
|
||||||
|
if len(data) != nbytes:
|
||||||
|
raise SystemExit(
|
||||||
|
f"{aname}: expected {nbytes} bytes, got {len(data)} for size={size}"
|
||||||
|
)
|
||||||
|
arr = ",".join(f"0x{b:02X}" for b in data)
|
||||||
|
fbody = fbody.rstrip() + f"\n{{{arr}}},/*\"闭\",{idx}*/"
|
||||||
|
text = text[: am.start()] + am.group(1) + fbody + am.group(3) + text[am.end() :]
|
||||||
|
print(f"append font {aname} idx={idx}")
|
||||||
|
|
||||||
|
open(HEADER, "w", encoding="utf-8", newline="\n").write(text)
|
||||||
|
print("patched", HEADER)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Build K1 release zip: MCU .bin + ExtFlash resources + SoundWalkerIAP + docs.
|
"""Build K1 release zip: Boot + MCU APP + ExtFlash resources + SoundWalkerIAP + docs.
|
||||||
|
|
||||||
External Flash has TWO regions that must both be present on a blank/erased chip:
|
External Flash has TWO regions that must both be present on a blank/erased chip:
|
||||||
0x00000000 tone pack (logo pad + Charg + 1/2/3.bin) -> *.tone.res / combined
|
0x00000000 tone pack (logo pad + Charg + 1/2/3.bin) -> *.tone.res / combined
|
||||||
0x00100000 UI0902 bitmaps (mode rows, boot logo, ...) -> *.ui0902.res / combined
|
0x00100000 UI0902 bitmaps (mode rows, boot logo, ...) -> *.ui0902.res / combined
|
||||||
|
|
||||||
Prefer flashing the combined *.extflash.res @ 0x0 for colleague upgrades.
|
Prefer flashing the combined *.extflash.res @ 0x0 for colleague upgrades.
|
||||||
|
Boot must also be updated so USB IAP wait screen can show UI0902_FLASH_MODE.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
@ -19,15 +21,27 @@ from pathlib import Path
|
||||||
REPO = Path(r"C:\Users\qjyu\Documents\SoundWalker\一诺国际吉他")
|
REPO = Path(r"C:\Users\qjyu\Documents\SoundWalker\一诺国际吉他")
|
||||||
PROJ = REPO / "Code" / "YNGJ-GT1-M - AT32F403ARCT7"
|
PROJ = REPO / "Code" / "YNGJ-GT1-M - AT32F403ARCT7"
|
||||||
EXE_BIN = PROJ / "project" / "IAR_V7.4" / "YNGJ-GT1-M" / "Exe" / "YNGJ-GT1-M.bin"
|
EXE_BIN = PROJ / "project" / "IAR_V7.4" / "YNGJ-GT1-M" / "Exe" / "YNGJ-GT1-M.bin"
|
||||||
TONE_BIN = PROJ / "tools" / "out" / "extflash_tone_0903.bin"
|
BOOT_BIN = (
|
||||||
|
PROJ
|
||||||
|
/ "AT32F403ARCT7_BOOT"
|
||||||
|
/ "project"
|
||||||
|
/ "IAR_V7.4"
|
||||||
|
/ "AT32F403ARCT7_BOOT"
|
||||||
|
/ "Exe"
|
||||||
|
/ "AT32F403ARCT7_BOOT.bin"
|
||||||
|
)
|
||||||
|
TONE_RES_VER = "0916"
|
||||||
|
TONE_BIN = PROJ / "tools" / "out" / f"extflash_tone_{TONE_RES_VER}.bin"
|
||||||
UI0902_BIN = PROJ / "tools" / "out" / "ui0902_res.bin"
|
UI0902_BIN = PROJ / "tools" / "out" / "ui0902_res.bin"
|
||||||
MAP_TXT = REPO / "Doc" / "音色文件" / "0903" / "FLASH_MAP.txt"
|
MAP_TXT = REPO / "Doc" / "音色文件" / "0916_bank" / "FLASH_MAP.txt"
|
||||||
|
TONE_HDR = PROJ / "project" / "inc" / "ExtFlash_Tone_Addr.h"
|
||||||
IAP_DIR = REPO / "升级" / "MCU主控升级"
|
IAP_DIR = REPO / "升级" / "MCU主控升级"
|
||||||
OUT_ROOT = REPO / "tools" / "out"
|
OUT_ROOT = REPO / "tools" / "out"
|
||||||
|
|
||||||
FW_VER = "0.2.6"
|
FW_VER = "0.2.14"
|
||||||
TONE_RES_VER = "0903"
|
|
||||||
UI0902_RES_BASE = 0x00100000
|
UI0902_RES_BASE = 0x00100000
|
||||||
|
BOOT_FLASH_ADDR = 0x08000000
|
||||||
|
APP_FLASH_ADDR = 0x08008000
|
||||||
|
|
||||||
|
|
||||||
def git_info(cwd: Path) -> tuple[str, str]:
|
def git_info(cwd: Path) -> tuple[str, str]:
|
||||||
|
|
@ -57,13 +71,81 @@ def build_combined_extflash(tone: bytes, ui: bytes) -> bytes:
|
||||||
return tone + (b"\xFF" * pad) + ui
|
return tone + (b"\xFF" * pad) + ui
|
||||||
|
|
||||||
|
|
||||||
|
def read_tone_addrs() -> dict[str, int]:
|
||||||
|
text = TONE_HDR.read_text(encoding="utf-8", errors="replace")
|
||||||
|
def grab(name: str) -> int:
|
||||||
|
m = re.search(rf"#define\s+{name}\s+0x([0-9A-Fa-f]+)UL", text)
|
||||||
|
if not m:
|
||||||
|
raise SystemExit(f"missing {name} in {TONE_HDR}")
|
||||||
|
return int(m.group(1), 16)
|
||||||
|
return {
|
||||||
|
"LOGO": grab("EXTFLASH_LOGO_ADDR"),
|
||||||
|
"CHARGING": grab("EXTFLASH_CHARGING_ADDR"),
|
||||||
|
"BIN1": grab("EXTFLASH_BIN1_RHYTHM_ADDR"),
|
||||||
|
"BIN2": grab("EXTFLASH_BIN2_SONG_HAITIAN_ADDR"),
|
||||||
|
"BIN3": grab("EXTFLASH_BIN3_UNIVERSAL_ADDR"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def verify_tone_layout(tone: bytes, check_sources: bool = True) -> None:
|
||||||
|
"""Refuse to ship ALL.res if logo/charg/1/2/3.bin missing, shifted, or mismatch."""
|
||||||
|
dab = b"\xABDAB"
|
||||||
|
addrs = read_tone_addrs()
|
||||||
|
tone_dir = REPO / "Doc" / "音色文件" / "0916_bank"
|
||||||
|
ziliao = None
|
||||||
|
for p in REPO.iterdir():
|
||||||
|
if p.is_dir() and (p / "logo.bin").is_file() and (p / "Charg.bin").is_file():
|
||||||
|
ziliao = p
|
||||||
|
break
|
||||||
|
if ziliao is None:
|
||||||
|
raise SystemExit("logo.bin/Charg.bin not found under repo")
|
||||||
|
|
||||||
|
sources = [
|
||||||
|
("LOGO", addrs["LOGO"], None, ziliao / "logo.bin"),
|
||||||
|
("CHARGING", addrs["CHARGING"], None, ziliao / "Charg.bin"),
|
||||||
|
("BIN1", addrs["BIN1"], 31, tone_dir / "1.bin"),
|
||||||
|
("BIN2", addrs["BIN2"], 1, tone_dir / "2.bin"),
|
||||||
|
("BIN3", addrs["BIN3"], 3, tone_dir / "3.bin"),
|
||||||
|
]
|
||||||
|
for name, off, want_cnt, src_path in sources:
|
||||||
|
if off + 16 > len(tone):
|
||||||
|
raise SystemExit(f"verify {name}: tone pack too short for 0x{off:X}")
|
||||||
|
if want_cnt is not None:
|
||||||
|
magic = tone[off : off + 4]
|
||||||
|
cnt = int.from_bytes(tone[off + 12 : off + 16], "little")
|
||||||
|
if magic != dab or cnt != want_cnt:
|
||||||
|
raise SystemExit(
|
||||||
|
f"verify {name} @0x{off:X} failed: magic={magic.hex()} cnt={cnt} "
|
||||||
|
f"(want ABDAB/{want_cnt}). Re-run pack_extflash_tone_0903.py."
|
||||||
|
)
|
||||||
|
if check_sources and src_path.is_file():
|
||||||
|
src = src_path.read_bytes()
|
||||||
|
if name == "BIN2" and len(src) > 41 * 1024:
|
||||||
|
raise SystemExit(f"verify BIN2: source 2.bin {len(src)} > 41KB")
|
||||||
|
if tone[off : off + len(src)] != src:
|
||||||
|
raise SystemExit(f"verify {name}: content @0x{off:X} != {src_path}")
|
||||||
|
print(
|
||||||
|
"verify tone layout: LOGO/CHARG/BIN1/BIN2/BIN3 OK "
|
||||||
|
f"(BIN3@0x{addrs['BIN3']:X})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
if not BOOT_BIN.is_file():
|
||||||
|
raise SystemExit(f"missing Boot bin: {BOOT_BIN}")
|
||||||
if not EXE_BIN.is_file():
|
if not EXE_BIN.is_file():
|
||||||
raise SystemExit(f"missing MCU bin: {EXE_BIN}")
|
raise SystemExit(f"missing MCU bin: {EXE_BIN}")
|
||||||
if not TONE_BIN.is_file():
|
tone_bin = TONE_BIN
|
||||||
raise SystemExit(f"missing tone pack: {TONE_BIN} (run pack_extflash_tone_0903.py)")
|
if not tone_bin.is_file():
|
||||||
|
legacy = PROJ / "tools" / "out" / "extflash_tone_0903.bin"
|
||||||
|
if legacy.is_file():
|
||||||
|
tone_bin = legacy
|
||||||
|
else:
|
||||||
|
raise SystemExit(f"missing tone pack: {TONE_BIN} (run pack_extflash_tone_0903.py)")
|
||||||
if not UI0902_BIN.is_file():
|
if not UI0902_BIN.is_file():
|
||||||
raise SystemExit(f"missing UI0902 pack: {UI0902_BIN} (run gen_ui0902_assets.py)")
|
raise SystemExit(f"missing UI0902 pack: {UI0902_BIN} (run gen_ui0902_assets.py)")
|
||||||
|
if not TONE_HDR.is_file():
|
||||||
|
raise SystemExit(f"missing {TONE_HDR}")
|
||||||
|
|
||||||
iap = IAP_DIR / "SoundWalkerIAP.exe"
|
iap = IAP_DIR / "SoundWalkerIAP.exe"
|
||||||
guide = IAP_DIR / "升级步骤.docx"
|
guide = IAP_DIR / "升级步骤.docx"
|
||||||
|
|
@ -82,16 +164,24 @@ def main() -> None:
|
||||||
shutil.rmtree(pkg)
|
shutil.rmtree(pkg)
|
||||||
pkg.mkdir(parents=True)
|
pkg.mkdir(parents=True)
|
||||||
|
|
||||||
tone_data = TONE_BIN.read_bytes()
|
tone_data = tone_bin.read_bytes()
|
||||||
|
verify_tone_layout(tone_data)
|
||||||
ui_data = UI0902_BIN.read_bytes()
|
ui_data = UI0902_BIN.read_bytes()
|
||||||
|
if len(ui_data) < 1024:
|
||||||
|
raise SystemExit(f"UI0902 pack too small: {len(ui_data)}")
|
||||||
combined = build_combined_extflash(tone_data, ui_data)
|
combined = build_combined_extflash(tone_data, ui_data)
|
||||||
|
verify_tone_layout(combined)
|
||||||
|
|
||||||
|
addrs = read_tone_addrs()
|
||||||
|
boot_name = f"AT32F403ARCT7_BOOT_v{FW_VER}_{stamp}.bin"
|
||||||
mcu_name = f"YNGJ-GT1-M_MCU_v{FW_VER}_{stamp}.bin"
|
mcu_name = f"YNGJ-GT1-M_MCU_v{FW_VER}_{stamp}.bin"
|
||||||
comb_name = f"extflash_ALL_tone0903_ui0902_{stamp}.res"
|
comb_name = f"extflash_ALL_tone{TONE_RES_VER}_ui0902_{stamp}.res"
|
||||||
|
|
||||||
|
boot_dst = pkg / boot_name
|
||||||
mcu_dst = pkg / mcu_name
|
mcu_dst = pkg / mcu_name
|
||||||
comb_dst = pkg / comb_name
|
comb_dst = pkg / comb_name
|
||||||
|
|
||||||
|
shutil.copy2(BOOT_BIN, boot_dst)
|
||||||
shutil.copy2(EXE_BIN, mcu_dst)
|
shutil.copy2(EXE_BIN, mcu_dst)
|
||||||
comb_dst.write_bytes(combined)
|
comb_dst.write_bytes(combined)
|
||||||
shutil.copy2(iap, pkg / "SoundWalkerIAP.exe")
|
shutil.copy2(iap, pkg / "SoundWalkerIAP.exe")
|
||||||
|
|
@ -99,10 +189,10 @@ def main() -> None:
|
||||||
if MAP_TXT.is_file():
|
if MAP_TXT.is_file():
|
||||||
shutil.copy2(MAP_TXT, pkg / "FLASH_MAP.txt")
|
shutil.copy2(MAP_TXT, pkg / "FLASH_MAP.txt")
|
||||||
|
|
||||||
|
shutil.copy2(boot_dst, OUT_ROOT / boot_name)
|
||||||
shutil.copy2(mcu_dst, OUT_ROOT / mcu_name)
|
shutil.copy2(mcu_dst, OUT_ROOT / mcu_name)
|
||||||
shutil.copy2(comb_dst, OUT_ROOT / "extflash_ALL_tone0903_ui0902.res")
|
shutil.copy2(comb_dst, OUT_ROOT / f"extflash_ALL_tone{TONE_RES_VER}_ui0902.res")
|
||||||
# Keep build intermediates under tools/out for local rebuilds; not shipped in package.
|
(OUT_ROOT / f"extflash_tone_{TONE_RES_VER}.res").write_bytes(tone_data)
|
||||||
(OUT_ROOT / "extflash_tone_0903.res").write_bytes(tone_data)
|
|
||||||
(OUT_ROOT / "extflash_ui0902.res").write_bytes(ui_data)
|
(OUT_ROOT / "extflash_ui0902.res").write_bytes(ui_data)
|
||||||
|
|
||||||
readme = pkg / "README.txt"
|
readme = pkg / "README.txt"
|
||||||
|
|
@ -115,28 +205,36 @@ def main() -> None:
|
||||||
f"Git subject {subject}",
|
f"Git subject {subject}",
|
||||||
f"FW version {FW_VER}",
|
f"FW version {FW_VER}",
|
||||||
"",
|
"",
|
||||||
"==== 整机升级请刷这两项 ====",
|
"==== 整机升级请刷这三项 ====",
|
||||||
f" 1) {mcu_name} -> MCU APP",
|
f" 1) {boot_name} -> Bootloader @ 0x{BOOT_FLASH_ADDR:08X}",
|
||||||
f" 2) {comb_name} -> 外部 Flash 从 0x0 起整包",
|
f" 2) {mcu_name} -> MCU APP @ 0x{APP_FLASH_ADDR:08X}",
|
||||||
|
f" 3) {comb_name} -> 外部 Flash 从 0x0 起整包",
|
||||||
" (= 音色区 + 填充 + UI0902 图片区)",
|
" (= 音色区 + 填充 + UI0902 图片区)",
|
||||||
"",
|
"",
|
||||||
"建议步骤:",
|
"建议步骤:",
|
||||||
|
" - 先刷 Boot(否则 USB 烧录等待画面仍是旧红字「升级模式」)",
|
||||||
|
" - 再刷 APP",
|
||||||
" - 先整片擦除外部 Flash,再刷上述 ALL .res @ 0x00000000",
|
" - 先整片擦除外部 Flash,再刷上述 ALL .res @ 0x00000000",
|
||||||
" - 勿只刷音色区,否则模式选择页会花屏(UI 图在 0x100000)",
|
" - 勿只刷音色区,否则模式选择页会花屏(UI 图在 0x100000)",
|
||||||
"",
|
"",
|
||||||
"外部 Flash 分区:",
|
"外部 Flash 分区:",
|
||||||
" 0x00000000 音色/充电图 (toneRes)",
|
" 0x00000000 音色/充电图 (toneRes)",
|
||||||
" logo pad + Charg@0xCB70 + 1.bin@0x1B8F0 + 2.bin(HKTK/2s) + 3.bin",
|
f" logo@0x{addrs['LOGO']:X} + Charg@0x{addrs['CHARGING']:X} + "
|
||||||
|
f"1.bin@0x{addrs['BIN1']:X} + 2.bin@0x{addrs['BIN2']:X} + 3.bin@0x{addrs['BIN3']:X}",
|
||||||
" 0x00100000 UI0902 图片 (模式选择、开机 Logo、充电、烧录模式、设置/调音台等)",
|
" 0x00100000 UI0902 图片 (模式选择、开机 Logo、充电、烧录模式、设置/调音台等)",
|
||||||
|
" 0x001D2000 UI0902_FLASH_MODE 烧录模式全屏图(Boot 读取)",
|
||||||
"",
|
"",
|
||||||
|
f" {boot_name} size={boot_dst.stat().st_size} @ 0x{BOOT_FLASH_ADDR:08X}",
|
||||||
|
f" sha256={sha256(boot_dst)}",
|
||||||
|
f" {mcu_name} size={mcu_dst.stat().st_size} @ 0x{APP_FLASH_ADDR:08X}",
|
||||||
|
f" sha256={sha256(mcu_dst)}",
|
||||||
f" {comb_name} size={comb_dst.stat().st_size} @ 0x00000000",
|
f" {comb_name} size={comb_dst.stat().st_size} @ 0x00000000",
|
||||||
f" sha256={sha256(comb_dst)}",
|
f" sha256={sha256(comb_dst)}",
|
||||||
"",
|
"",
|
||||||
f"MCU: {mcu_name} size={mcu_dst.stat().st_size} sha256={sha256(mcu_dst)}",
|
|
||||||
"",
|
|
||||||
"说明:",
|
"说明:",
|
||||||
|
" - Boot 进入 USB IAP 时显示 UI0902_FLASH_MODE;资源未烧录时白字黑底兜底。",
|
||||||
" - 开机全屏 Logo、关机充电全屏画面在 UI0902,不在音色包里的 logo.bin/Charg.bin 占位。",
|
" - 开机全屏 Logo、关机充电全屏画面在 UI0902,不在音色包里的 logo.bin/Charg.bin 占位。",
|
||||||
" - AutoBand 0x9EB5F 本版未重排。",
|
f" - 本包音色来源 Doc/音色文件/{TONE_RES_VER}/;BIN3 随 2.bin 长度后移。",
|
||||||
"",
|
"",
|
||||||
"附件: SoundWalkerIAP.exe / 升级步骤.docx / FLASH_MAP.txt",
|
"附件: SoundWalkerIAP.exe / 升级步骤.docx / FLASH_MAP.txt",
|
||||||
"",
|
"",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Bottom nav: 0909 enlarged icons + original design text strips, fixed baseline."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
PROJ = Path(__file__).resolve().parents[1]
|
||||||
|
ROOT = PROJ.parent.parent
|
||||||
|
SRC_DIR = next(
|
||||||
|
p
|
||||||
|
for p in (ROOT / "Doc" / "UI" / "K1标准界面图 0904").iterdir()
|
||||||
|
if p.is_dir() and "0909" in p.name
|
||||||
|
)
|
||||||
|
ASSETS = PROJ / "tools" / "assets_0902" / "03_底部图标"
|
||||||
|
DOC_ASSETS = ROOT / "Doc" / "UI" / "k1_ui_png 0902" / "03_底部图标"
|
||||||
|
OUT_C = PROJ / "device" / "LCD_ILI9341" / "Drv_ILI9341_Lcd_Image_UI0902.c"
|
||||||
|
OUT_H = PROJ / "device" / "LCD_ILI9341" / "Drv_ILI9341_Lcd_Image_UI0902.h"
|
||||||
|
|
||||||
|
ROW_BG = (36, 43, 58)
|
||||||
|
CANVAS_H = 32
|
||||||
|
ICON_AREA_H = 20
|
||||||
|
TEXT_TOP = 22 # all labels share this top row
|
||||||
|
|
||||||
|
# png_idx, src_glyph_num, symbol_base, width
|
||||||
|
MAPPING = [
|
||||||
|
(10, 21, "gImage_UI0902_TabSetting_Sel", 40),
|
||||||
|
(11, 23, "gImage_UI0902_TabSetting_Not", 40),
|
||||||
|
(12, 25, "gImage_UI0902_TabMixer_Sel", 40),
|
||||||
|
(13, 27, "gImage_UI0902_TabMixer_Not", 22),
|
||||||
|
(14, 20, "gImage_UI0902_TabMode_Sel", 40),
|
||||||
|
(15, 22, "gImage_UI0902_TabMode_Not", 40),
|
||||||
|
(16, 24, "gImage_UI0902_TabBack_Sel", 40),
|
||||||
|
(17, 26, "gImage_UI0902_TabBack_Not", 22),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def git_png(rel: str) -> Image.Image:
|
||||||
|
data = subprocess.check_output(["git", "show", f"HEAD:{rel}"], cwd=PROJ)
|
||||||
|
return Image.open(BytesIO(data)).convert("RGBA")
|
||||||
|
|
||||||
|
|
||||||
|
def clear_bg(im: Image.Image) -> Image.Image:
|
||||||
|
im = im.convert("RGBA")
|
||||||
|
px = im.load()
|
||||||
|
for y in range(im.height):
|
||||||
|
for x in range(im.width):
|
||||||
|
r, g, b, a = px[x, y]
|
||||||
|
if a < 40 or (r < 12 and g < 12 and b < 12):
|
||||||
|
px[x, y] = (0, 0, 0, 0)
|
||||||
|
return im
|
||||||
|
|
||||||
|
|
||||||
|
def row_has_ink(im: Image.Image, y: int) -> bool:
|
||||||
|
w = im.width
|
||||||
|
for x in range(w):
|
||||||
|
r, g, b, a = im.getpixel((x, y))
|
||||||
|
if a > 40 and (r + g + b) > 40:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def extract_text(im: Image.Image) -> Image.Image:
|
||||||
|
"""Crop label band below the icon/gap from original composite."""
|
||||||
|
im = clear_bg(im)
|
||||||
|
h = im.height
|
||||||
|
# find last empty gap row in upper half, then text starts after
|
||||||
|
empty = [y for y in range(h) if not row_has_ink(im, y)]
|
||||||
|
# text region: from first non-empty after mid-gap
|
||||||
|
mid_empties = [y for y in empty if 10 <= y <= 20]
|
||||||
|
if mid_empties:
|
||||||
|
text_y0 = mid_empties[-1] + 1
|
||||||
|
else:
|
||||||
|
text_y0 = h // 2 + 2
|
||||||
|
while text_y0 < h and not row_has_ink(im, text_y0):
|
||||||
|
text_y0 += 1
|
||||||
|
text_y1 = h - 1
|
||||||
|
while text_y1 > text_y0 and not row_has_ink(im, text_y1):
|
||||||
|
text_y1 -= 1
|
||||||
|
band = im.crop((0, text_y0, im.width, text_y1 + 1))
|
||||||
|
# trim horizontal transparent
|
||||||
|
bbox = band.getbbox()
|
||||||
|
if bbox:
|
||||||
|
band = band.crop(bbox)
|
||||||
|
return clear_bg(band)
|
||||||
|
|
||||||
|
|
||||||
|
def fit_icon(im: Image.Image) -> Image.Image:
|
||||||
|
im = clear_bg(im)
|
||||||
|
nw, nh = im.size
|
||||||
|
if nw > 24 or nh > ICON_AREA_H:
|
||||||
|
scale = min(24 / nw, ICON_AREA_H / nh)
|
||||||
|
nw = max(1, int(round(nw * scale)))
|
||||||
|
nh = max(1, int(round(nh * scale)))
|
||||||
|
im = clear_bg(im.resize((nw, nh), Image.Resampling.NEAREST))
|
||||||
|
nw, nh = im.size
|
||||||
|
return im
|
||||||
|
|
||||||
|
|
||||||
|
def rgba_to_rgb565(im: Image.Image) -> tuple[bytes, int, int]:
|
||||||
|
im = im.convert("RGBA")
|
||||||
|
px = im.load()
|
||||||
|
w, h = im.size
|
||||||
|
out = bytearray(w * h * 2)
|
||||||
|
i = 0
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
r, g, b, a = px[x, y]
|
||||||
|
if a < 40 or (r < 12 and g < 12 and b < 12):
|
||||||
|
out[i] = out[i + 1] = 0
|
||||||
|
elif (
|
||||||
|
abs(r - ROW_BG[0]) <= 18
|
||||||
|
and abs(g - ROW_BG[1]) <= 18
|
||||||
|
and abs(b - ROW_BG[2]) <= 18
|
||||||
|
):
|
||||||
|
out[i] = out[i + 1] = 0
|
||||||
|
else:
|
||||||
|
r = (r * a) // 255
|
||||||
|
g = (g * a) // 255
|
||||||
|
b = (b * a) // 255
|
||||||
|
v = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
|
||||||
|
out[i] = (v >> 8) & 0xFF
|
||||||
|
out[i + 1] = v & 0xFF
|
||||||
|
i += 2
|
||||||
|
return bytes(out), w, h
|
||||||
|
|
||||||
|
|
||||||
|
def emit_c_array(name: str, data: bytes, w: int, h: int) -> str:
|
||||||
|
lines = [f"const unsigned char {name}[{len(data)}] = {{ /* {w}x{h} RGB565 BE */"]
|
||||||
|
for i in range(0, len(data), 16):
|
||||||
|
lines.append(",".join(f"0x{b:02X}" for b in data[i : i + 16]) + ",")
|
||||||
|
lines.append("};")
|
||||||
|
lines.append("")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_c_array(text: str, old_name: str, new_block: str) -> str:
|
||||||
|
pat = re.compile(
|
||||||
|
rf"const unsigned char {re.escape(old_name)}\[\d+\] = \{{.*?\n\}};\n?",
|
||||||
|
re.S,
|
||||||
|
)
|
||||||
|
if not pat.search(text):
|
||||||
|
raise SystemExit(f"array not found: {old_name}")
|
||||||
|
return pat.sub(new_block, text, count=1)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_h_decl(text: str, old_name: str, new_name: str, nbytes: int) -> str:
|
||||||
|
pat = re.compile(rf"extern const unsigned char {re.escape(old_name)}\[\d+\];")
|
||||||
|
if not pat.search(text):
|
||||||
|
raise SystemExit(f"decl not found: {old_name}")
|
||||||
|
return pat.sub(
|
||||||
|
f"extern const unsigned char {new_name}[{nbytes}];", text, count=1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
by_num = {int(f.stem.rsplit("-", 1)[-1]): f for f in SRC_DIR.glob("*.png")}
|
||||||
|
c_text = OUT_C.read_text(encoding="utf-8")
|
||||||
|
h_text = OUT_H.read_text(encoding="utf-8")
|
||||||
|
sizes: dict[int, tuple[int, int]] = {}
|
||||||
|
|
||||||
|
for out_idx, src_num, base, width in MAPPING:
|
||||||
|
icon = fit_icon(Image.open(by_num[src_num]))
|
||||||
|
old = git_png(f"tools/assets_0902/03_底部图标/底部图标-{out_idx}.png")
|
||||||
|
text = extract_text(old)
|
||||||
|
|
||||||
|
# widen canvas if text wider
|
||||||
|
cw = max(width, icon.width, text.width)
|
||||||
|
canvas = Image.new("RGBA", (cw, CANVAS_H), (0, 0, 0, 0))
|
||||||
|
iy = (ICON_AREA_H - icon.height) // 2
|
||||||
|
canvas.paste(icon, ((cw - icon.width) // 2, max(0, iy)), icon)
|
||||||
|
# pin text BOTTOM to same row so labels sit on one baseline
|
||||||
|
text_bottom = CANVAS_H - 1
|
||||||
|
ty = text_bottom - text.height + 1
|
||||||
|
if ty < TEXT_TOP:
|
||||||
|
ty = TEXT_TOP
|
||||||
|
canvas.paste(text, ((cw - text.width) // 2, ty), text)
|
||||||
|
|
||||||
|
out = ASSETS / f"底部图标-{out_idx}.png"
|
||||||
|
canvas.save(out)
|
||||||
|
if DOC_ASSETS.exists():
|
||||||
|
canvas.save(DOC_ASSETS / f"底部图标-{out_idx}.png")
|
||||||
|
|
||||||
|
data, w, h = rgba_to_rgb565(canvas)
|
||||||
|
new_name = f"{base}_{w}x{h}"
|
||||||
|
m = re.search(rf"{re.escape(base)}_\d+x\d+", c_text)
|
||||||
|
if not m:
|
||||||
|
raise SystemExit(f"cannot locate {base}")
|
||||||
|
old_name = m.group(0)
|
||||||
|
c_text = replace_c_array(c_text, old_name, emit_c_array(new_name, data, w, h))
|
||||||
|
h_text = replace_h_decl(h_text, old_name, new_name, len(data))
|
||||||
|
sizes[out_idx] = (w, h)
|
||||||
|
print(f"{out_idx}: {w}x{h} text={text.size} -> {new_name}")
|
||||||
|
|
||||||
|
OUT_C.write_text(c_text, encoding="utf-8")
|
||||||
|
OUT_H.write_text(h_text, encoding="utf-8")
|
||||||
|
print("SIZES", sizes)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Probe 1.bin (rhythm) and 3.bin (universal) for bass ch8 + plan compliance.
|
||||||
|
|
||||||
|
Requires FW with: tone bin1 / tone bin3 / tone start / chord key / chord xpose / PITCH ch8(bass)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from rtt_pitch_reg_test import ( # noqa: E402
|
||||||
|
RttSession,
|
||||||
|
analyze_lines,
|
||||||
|
connect_jlink,
|
||||||
|
expect_for_event,
|
||||||
|
find_rtt_control_block,
|
||||||
|
import_deps,
|
||||||
|
parse_pitch_line,
|
||||||
|
wait_rtt_ready,
|
||||||
|
)
|
||||||
|
|
||||||
|
PITCH_RE_CH = re.compile(r"\[PITCH\].*?ch(\d+)\((\w+)\)")
|
||||||
|
CH_SEEN_RE = re.compile(r"ch-seen ch(\d+) role=(\w+) key=(-?\d+)")
|
||||||
|
TONE_OK_RE = re.compile(r"TONE_BIN([13]).*name=(\S+).*count=(\d+)")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PresetResult:
|
||||||
|
bin_id: str
|
||||||
|
idx: int
|
||||||
|
name: str = ""
|
||||||
|
channels: Counter = field(default_factory=Counter)
|
||||||
|
roles: Counter = field(default_factory=Counter)
|
||||||
|
bass_n: int = 0
|
||||||
|
drum_n: int = 0
|
||||||
|
chord_n: int = 0
|
||||||
|
pass_ok: int = 0
|
||||||
|
pass_fail: int = 0
|
||||||
|
iii_ok: int = 0
|
||||||
|
iii_fail: int = 0
|
||||||
|
xf_ok: int = 0
|
||||||
|
xf_fail: int = 0
|
||||||
|
bass_ok: int = 0
|
||||||
|
bass_fail: int = 0
|
||||||
|
samples: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
def run_preset(sess: RttSession, bin_id: str, idx: int, hold: float) -> PresetResult:
|
||||||
|
res = PresetResult(bin_id=bin_id, idx=idx)
|
||||||
|
mark = len(sess.lines)
|
||||||
|
|
||||||
|
if bin_id == "1":
|
||||||
|
ok = sess.cmd_ack(f"tone bin1 {idx}", "TONE_BIN1", timeout_s=3.0, retries=4)
|
||||||
|
else:
|
||||||
|
ok = sess.cmd_ack(f"tone bin3 {idx}", "TONE_BIN3", timeout_s=3.0, retries=4)
|
||||||
|
if not ok:
|
||||||
|
res.samples.append("FAIL load ack")
|
||||||
|
return res
|
||||||
|
|
||||||
|
# parse name from recent lines
|
||||||
|
for line in sess.lines[-8:]:
|
||||||
|
m = TONE_OK_RE.search(line)
|
||||||
|
if m and m.group(1) == bin_id:
|
||||||
|
res.name = m.group(2)
|
||||||
|
break
|
||||||
|
|
||||||
|
sess.set_xpose(2)
|
||||||
|
sess.cmd("tone start", settle=0.7)
|
||||||
|
|
||||||
|
# I/II
|
||||||
|
sess.set_key(0)
|
||||||
|
sess.set_key(2)
|
||||||
|
sess.pump(hold)
|
||||||
|
|
||||||
|
# III
|
||||||
|
sess.set_key(0)
|
||||||
|
sess.set_key(8)
|
||||||
|
sess.pump(hold)
|
||||||
|
|
||||||
|
# VII + #F
|
||||||
|
sess.set_xpose(6)
|
||||||
|
sess.set_key(0)
|
||||||
|
sess.set_key(20)
|
||||||
|
sess.pump(hold)
|
||||||
|
|
||||||
|
sess.set_key(23) # stop
|
||||||
|
sess.pump(0.3)
|
||||||
|
|
||||||
|
chunk = sess.lines[mark:]
|
||||||
|
for line in chunk:
|
||||||
|
m = CH_SEEN_RE.search(line)
|
||||||
|
if m:
|
||||||
|
ch, role = int(m.group(1)), m.group(2)
|
||||||
|
res.channels[ch] += 1
|
||||||
|
res.roles[role] += 1
|
||||||
|
res.samples.append(line.strip())
|
||||||
|
ev = parse_pitch_line(line)
|
||||||
|
if not ev or not ev.is_on:
|
||||||
|
continue
|
||||||
|
res.channels[ev.ch] += 1
|
||||||
|
res.roles[ev.role] += 1
|
||||||
|
if ev.role == "bass" or ev.ch == 8:
|
||||||
|
res.bass_n += 1
|
||||||
|
elif ev.role == "drum" or ev.ch == 9:
|
||||||
|
res.drum_n += 1
|
||||||
|
else:
|
||||||
|
res.chord_n += 1
|
||||||
|
|
||||||
|
detail, ok = expect_for_event(ev)
|
||||||
|
tag = f"ch{ev.ch}({ev.role}) {ev.key_in}->{ev.key_out} chord={ev.chord} | {detail}"
|
||||||
|
if ev.chord <= 6:
|
||||||
|
if ok:
|
||||||
|
res.pass_ok += 1
|
||||||
|
else:
|
||||||
|
res.pass_fail += 1
|
||||||
|
res.samples.append("FAIL " + tag)
|
||||||
|
elif ev.xf:
|
||||||
|
if ok:
|
||||||
|
res.xf_ok += 1
|
||||||
|
else:
|
||||||
|
res.xf_fail += 1
|
||||||
|
res.samples.append("FAIL " + tag)
|
||||||
|
elif ev.role == "bass" or ev.ch == 8:
|
||||||
|
if ok:
|
||||||
|
res.bass_ok += 1
|
||||||
|
else:
|
||||||
|
res.bass_fail += 1
|
||||||
|
res.samples.append("FAIL " + tag)
|
||||||
|
else:
|
||||||
|
if ok:
|
||||||
|
res.iii_ok += 1
|
||||||
|
else:
|
||||||
|
res.iii_fail += 1
|
||||||
|
res.samples.append("FAIL " + tag)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--hold", type=float, default=2.2)
|
||||||
|
ap.add_argument("--bin1", default="0,1,2,3,4,5,8,12,16,20,24,28", help="1.bin indices")
|
||||||
|
ap.add_argument("--bin3", default="0,1,2", help="3.bin indices")
|
||||||
|
ap.add_argument("--out", default="")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
bin1_idxs = [int(x) for x in args.bin1.split(",") if x.strip() != ""]
|
||||||
|
bin3_idxs = [int(x) for x in args.bin3.split(",") if x.strip() != ""]
|
||||||
|
|
||||||
|
import_deps()
|
||||||
|
jlink = connect_jlink("AT32F403AC")
|
||||||
|
results: list[PresetResult] = []
|
||||||
|
try:
|
||||||
|
cb = find_rtt_control_block(jlink)
|
||||||
|
if cb is None:
|
||||||
|
raise SystemExit("RTT CB not found")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
wait_rtt_ready(jlink)
|
||||||
|
sess = RttSession(jlink)
|
||||||
|
sess.pump(0.3)
|
||||||
|
sess.cmd("log clear", 0.3)
|
||||||
|
|
||||||
|
print("\n######## BIN1 / 1.bin rhythms ########", flush=True)
|
||||||
|
for idx in bin1_idxs:
|
||||||
|
print(f"\n--- BIN1 idx={idx} ---", flush=True)
|
||||||
|
r = run_preset(sess, "1", idx, args.hold)
|
||||||
|
results.append(r)
|
||||||
|
print(
|
||||||
|
f"name={r.name} ch={dict(r.channels)} roles={dict(r.roles)} "
|
||||||
|
f"bass={r.bass_n} drum={r.drum_n} chord={r.chord_n} "
|
||||||
|
f"I/II {r.pass_ok}/{r.pass_fail} III {r.iii_ok}/{r.iii_fail} "
|
||||||
|
f"xf {r.xf_ok}/{r.xf_fail} bassMap {r.bass_ok}/{r.bass_fail}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n######## BIN3 / 3.bin universal ########", flush=True)
|
||||||
|
for idx in bin3_idxs:
|
||||||
|
print(f"\n--- BIN3 idx={idx} ---", flush=True)
|
||||||
|
r = run_preset(sess, "3", idx, args.hold)
|
||||||
|
results.append(r)
|
||||||
|
print(
|
||||||
|
f"name={r.name} ch={dict(r.channels)} roles={dict(r.roles)} "
|
||||||
|
f"bass={r.bass_n} drum={r.drum_n} chord={r.chord_n} "
|
||||||
|
f"I/II {r.pass_ok}/{r.pass_fail} III {r.iii_ok}/{r.iii_fail} "
|
||||||
|
f"xf {r.xf_ok}/{r.xf_fail} bassMap {r.bass_ok}/{r.bass_fail}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Summary vs plan
|
||||||
|
print("\n======== PLAN CHECK (bass和弦分通道八度) ========", flush=True)
|
||||||
|
with_bass = [r for r in results if r.bass_n > 0]
|
||||||
|
print(f"Presets tested: {len(results)}; with ch8(bass) NoteOn: {len(with_bass)}", flush=True)
|
||||||
|
if with_bass:
|
||||||
|
for r in with_bass:
|
||||||
|
print(
|
||||||
|
f" BASS HIT {r.bin_id}.bin[{r.idx}] {r.name}: "
|
||||||
|
f"bass_n={r.bass_n} map_ok={r.bass_ok} fail={r.bass_fail}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(" NO preset produced ch8(bass). Plan bass rule NOT verified on-device.", flush=True)
|
||||||
|
|
||||||
|
# Aggregate chord rules
|
||||||
|
pass_ok = sum(r.pass_ok for r in results)
|
||||||
|
pass_fail = sum(r.pass_fail for r in results)
|
||||||
|
iii_ok = sum(r.iii_ok for r in results)
|
||||||
|
iii_fail = sum(r.iii_fail for r in results)
|
||||||
|
xf_ok = sum(r.xf_ok for r in results)
|
||||||
|
xf_fail = sum(r.xf_fail for r in results)
|
||||||
|
bass_ok = sum(r.bass_ok for r in results)
|
||||||
|
bass_fail = sum(r.bass_fail for r in results)
|
||||||
|
|
||||||
|
def gate(name, ok, fail, need=True):
|
||||||
|
status = "PASS" if fail == 0 and (ok > 0 or not need) else ("FAIL" if fail else "SKIP")
|
||||||
|
print(f" [{status}] {name}: ok={ok} fail={fail}", flush=True)
|
||||||
|
return status != "FAIL"
|
||||||
|
|
||||||
|
all_ok = True
|
||||||
|
all_ok &= gate("I/II PASS (keys1-6)", pass_ok, pass_fail)
|
||||||
|
all_ok &= gate("III+ chord map", iii_ok, iii_fail)
|
||||||
|
all_ok &= gate("xpose>=#F extra-12", xf_ok, xf_fail)
|
||||||
|
all_ok &= gate("bass ch8 map", bass_ok, bass_fail, need=False)
|
||||||
|
if not with_bass:
|
||||||
|
all_ok = False
|
||||||
|
print(" [FAIL] plan requires bass on code ch8 — never seen across 1.bin/3.bin samples", flush=True)
|
||||||
|
|
||||||
|
# channel histogram
|
||||||
|
ch_all: Counter = Counter()
|
||||||
|
for r in results:
|
||||||
|
ch_all.update(r.channels)
|
||||||
|
print(f"Channel histogram: {dict(sorted(ch_all.items()))}", flush=True)
|
||||||
|
|
||||||
|
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
out = args.out or os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||||
|
f"bin13_reg_{stamp}.log",
|
||||||
|
)
|
||||||
|
with open(out, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(f"# bin1/bin3 plan check {datetime.now().isoformat(timespec='seconds')}\n")
|
||||||
|
for line in sess.lines:
|
||||||
|
f.write(line + "\n")
|
||||||
|
f.write("\n# SUMMARY\n")
|
||||||
|
for r in results:
|
||||||
|
f.write(
|
||||||
|
f"# {r.bin_id}[{r.idx}] {r.name} ch={dict(r.channels)} "
|
||||||
|
f"bass={r.bass_n} I={r.pass_ok}/{r.pass_fail} "
|
||||||
|
f"III={r.iii_ok}/{r.iii_fail} xf={r.xf_ok}/{r.xf_fail} "
|
||||||
|
f"bassMap={r.bass_ok}/{r.bass_fail}\n"
|
||||||
|
)
|
||||||
|
print(f"Log: {out}", flush=True)
|
||||||
|
return 0 if all_ok else 1
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
jlink.rtt_stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Dump K1 persisted W25Q field log via J-Link RTT (`log flash dump` / `log flash scan`)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from rtt_log_dump import ( # noqa: E402
|
||||||
|
connect_jlink,
|
||||||
|
find_rtt_control_block,
|
||||||
|
import_deps,
|
||||||
|
send_command,
|
||||||
|
wait_rtt_ready,
|
||||||
|
)
|
||||||
|
|
||||||
|
DUMP_CMD = b"log flash dump\n"
|
||||||
|
SCAN_CMD = b"log flash scan 65536\n"
|
||||||
|
|
||||||
|
|
||||||
|
def dump_flash_log(out_path: str, device: str, timeout_s: float, cmd: bytes = DUMP_CMD) -> None:
|
||||||
|
jlink = connect_jlink(device)
|
||||||
|
lines: list[str] = []
|
||||||
|
try:
|
||||||
|
cb = find_rtt_control_block(jlink)
|
||||||
|
if cb is None:
|
||||||
|
raise SystemExit("SEGGER RTT control block not found in SRAM")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}")
|
||||||
|
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
wait_rtt_ready(jlink)
|
||||||
|
|
||||||
|
for _ in range(5):
|
||||||
|
stale = jlink.rtt_read(0, 4096)
|
||||||
|
if stale:
|
||||||
|
text = bytes(stale).decode("utf-8", errors="replace")
|
||||||
|
if text.strip():
|
||||||
|
print("RTT0 stale:", text.strip()[:200])
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
print(f"Sending: {cmd!r}")
|
||||||
|
send_command(jlink, cmd)
|
||||||
|
|
||||||
|
buffer = b""
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
while time.time() < deadline:
|
||||||
|
chunk = jlink.rtt_read(0, 8192)
|
||||||
|
if chunk:
|
||||||
|
buffer += bytes(chunk)
|
||||||
|
if len(buffer) >= 4096 and (len(buffer) % 8192) < 300:
|
||||||
|
print(f" received {len(buffer)} bytes...")
|
||||||
|
if b"LOG_FLASH_DUMP_END" in buffer:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
if b"LOG_FLASH_DUMP_END" not in buffer:
|
||||||
|
preview = buffer[-500:].decode("utf-8", errors="replace") if buffer else ""
|
||||||
|
raise SystemExit(
|
||||||
|
"Timeout waiting for LOG_FLASH_DUMP_END.\n"
|
||||||
|
f"got {len(buffer)} bytes. tail={preview!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
text = buffer.decode("utf-8", errors="replace")
|
||||||
|
capture = False
|
||||||
|
for line in text.splitlines():
|
||||||
|
if line.strip() == "LOG_FLASH_DUMP_BEGIN":
|
||||||
|
capture = True
|
||||||
|
continue
|
||||||
|
if line.strip() == "LOG_FLASH_DUMP_END":
|
||||||
|
break
|
||||||
|
if capture:
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
header = (
|
||||||
|
f"# K1 flash log dump {datetime.now().isoformat(timespec='seconds')}\n"
|
||||||
|
f"# device={device}\n"
|
||||||
|
f"# cmd={cmd.decode('ascii', errors='replace').strip()}\n"
|
||||||
|
)
|
||||||
|
body = "\n".join(lines) + ("\n" if lines else "")
|
||||||
|
with open(out_path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(header)
|
||||||
|
f.write(body)
|
||||||
|
print(f"Saved {len(lines)} lines to {out_path}")
|
||||||
|
print(f"Full path: {os.path.abspath(out_path)}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
jlink.rtt_stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Dump K1 Flash field log via RTT")
|
||||||
|
parser.add_argument("--out", default="crash.log")
|
||||||
|
parser.add_argument("--device", default="AT32F403AC")
|
||||||
|
parser.add_argument("--timeout", type=float, default=300.0)
|
||||||
|
parser.add_argument(
|
||||||
|
"--scan",
|
||||||
|
action="store_true",
|
||||||
|
help="Ignore write pointer; scan first 64KB data for recoverable lines",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
import_deps()
|
||||||
|
cmd = SCAN_CMD if args.scan else DUMP_CMD
|
||||||
|
dump_flash_log(args.out, args.device, args.timeout, cmd=cmd)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Drive K1 via RTT while recording laptop mic; compare I/II vs III+ pitch."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Reuse helpers from sibling tools
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from mic_pitch_analyze import ( # noqa: E402
|
||||||
|
analyze_audio,
|
||||||
|
compare_summaries,
|
||||||
|
print_summary,
|
||||||
|
record,
|
||||||
|
save_wav,
|
||||||
|
summarize,
|
||||||
|
write_frame_log,
|
||||||
|
)
|
||||||
|
from rtt_pitch_reg_test import ( # noqa: E402
|
||||||
|
RttSession,
|
||||||
|
connect_jlink,
|
||||||
|
find_rtt_control_block,
|
||||||
|
import_deps,
|
||||||
|
wait_rtt_ready,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
out_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
hold = 8.0
|
||||||
|
sr = 16000
|
||||||
|
device = None # default mic
|
||||||
|
|
||||||
|
import_deps()
|
||||||
|
jlink = connect_jlink("AT32F403AC")
|
||||||
|
try:
|
||||||
|
cb = find_rtt_control_block(jlink)
|
||||||
|
if cb is None:
|
||||||
|
raise SystemExit("RTT CB not found")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
wait_rtt_ready(jlink)
|
||||||
|
sess = RttSession(jlink)
|
||||||
|
sess.pump(0.3)
|
||||||
|
|
||||||
|
sess.cmd("log clear", 0.2)
|
||||||
|
sess.cmd("tone local", 1.0)
|
||||||
|
sess.set_xpose(2)
|
||||||
|
sess.cmd("tone pick", 0.8)
|
||||||
|
|
||||||
|
# ---- Take A: I/II ----
|
||||||
|
print("\n=== TAKE A: I/II (key2) — put guitar near mic ===", flush=True)
|
||||||
|
sess.set_key(0)
|
||||||
|
sess.set_key(2)
|
||||||
|
sess.pump(0.8)
|
||||||
|
print("MIC record A starting...", flush=True)
|
||||||
|
audio_a = record(hold, sr, device)
|
||||||
|
wav_a = os.path.join(out_dir, f"mic_A_I_{stamp}.wav")
|
||||||
|
save_wav(wav_a, audio_a, sr)
|
||||||
|
frames_a = analyze_audio(audio_a, sr, fmin=40, fmax=500)
|
||||||
|
sum_a = summarize(frames_a, label="A:I/II")
|
||||||
|
print_summary(sum_a)
|
||||||
|
write_frame_log(os.path.join(out_dir, f"mic_A_I_{stamp}.csv"), frames_a, sum_a)
|
||||||
|
|
||||||
|
# ---- Take B: III ----
|
||||||
|
print("\n=== TAKE B: III (key8) ===", flush=True)
|
||||||
|
sess.set_key(0)
|
||||||
|
sess.set_key(8)
|
||||||
|
sess.pump(0.8)
|
||||||
|
print("MIC record B starting...", flush=True)
|
||||||
|
audio_b = record(hold, sr, device)
|
||||||
|
wav_b = os.path.join(out_dir, f"mic_B_III_{stamp}.wav")
|
||||||
|
save_wav(wav_b, audio_b, sr)
|
||||||
|
frames_b = analyze_audio(audio_b, sr, fmin=40, fmax=500)
|
||||||
|
sum_b = summarize(frames_b, label="B:III")
|
||||||
|
print_summary(sum_b)
|
||||||
|
write_frame_log(os.path.join(out_dir, f"mic_B_III_{stamp}.csv"), frames_b, sum_b)
|
||||||
|
|
||||||
|
# ---- Take C: VII + xpose #F (extra -12) ----
|
||||||
|
print("\n=== TAKE C: VII key20 + xpose=#F ===", flush=True)
|
||||||
|
sess.set_xpose(6)
|
||||||
|
sess.set_key(0)
|
||||||
|
sess.set_key(20)
|
||||||
|
sess.pump(0.8)
|
||||||
|
print("MIC record C starting...", flush=True)
|
||||||
|
audio_c = record(hold, sr, device)
|
||||||
|
wav_c = os.path.join(out_dir, f"mic_C_VII_xf_{stamp}.wav")
|
||||||
|
save_wav(wav_c, audio_c, sr)
|
||||||
|
frames_c = analyze_audio(audio_c, sr, fmin=40, fmax=500)
|
||||||
|
sum_c = summarize(frames_c, label="C:VII+#F")
|
||||||
|
print_summary(sum_c)
|
||||||
|
write_frame_log(os.path.join(out_dir, f"mic_C_VII_xf_{stamp}.csv"), frames_c, sum_c)
|
||||||
|
|
||||||
|
# Also bass-biased analysis (prefer low F0)
|
||||||
|
print("\n=== Bass-biased (fmax=180Hz) ===", flush=True)
|
||||||
|
for label, audio in (("A", audio_a), ("B", audio_b), ("C", audio_c)):
|
||||||
|
fr = analyze_audio(audio, sr, fmin=40, fmax=180)
|
||||||
|
print_summary(summarize(fr, label=f"{label}-bassband"))
|
||||||
|
|
||||||
|
print("\n=== A vs B (I/II vs III) ===", flush=True)
|
||||||
|
compare_summaries(sum_a, sum_b)
|
||||||
|
print("\n=== A vs C (I/II vs VII+#F) ===", flush=True)
|
||||||
|
compare_summaries(sum_a, sum_c)
|
||||||
|
|
||||||
|
print("\n=== stop ===", flush=True)
|
||||||
|
sess.stop_band()
|
||||||
|
|
||||||
|
# Save RTT lines
|
||||||
|
rtt_path = os.path.join(out_dir, f"mic_rtt_{stamp}.log")
|
||||||
|
with open(rtt_path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(f"# rtt+mic test {datetime.now().isoformat(timespec='seconds')}\n")
|
||||||
|
for line in sess.lines:
|
||||||
|
f.write(line + "\n")
|
||||||
|
print(f"RTT log: {rtt_path}", flush=True)
|
||||||
|
print(f"WAV A/B/C: {wav_a}\n {wav_b}\n {wav_c}", flush=True)
|
||||||
|
return 0
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
jlink.rtt_stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,594 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""K1 bass/chord register auto-test via J-Link RTT.
|
||||||
|
|
||||||
|
Requires firmware with:
|
||||||
|
- [PITCH]/[REG] logs (App_Auto.c)
|
||||||
|
- RTT cmds: chord key N / chord xpose N / tone pick / log clear
|
||||||
|
|
||||||
|
Modes:
|
||||||
|
python tools/rtt_pitch_reg_test.py # inject + live assert
|
||||||
|
python tools/rtt_pitch_reg_test.py --listen # you press keys; we assert
|
||||||
|
python tools/rtt_pitch_reg_test.py --analyze oct_reg3.log
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
DEFAULT_DEVICES = ("Cortex-M4", "AT32F403AC", "AT32F403A")
|
||||||
|
|
||||||
|
MIDI_E1 = 28
|
||||||
|
MIDI_E2 = 40
|
||||||
|
MIDI_GSHARP4 = 68
|
||||||
|
XPOSE_FS = 6
|
||||||
|
BASS_CH_DISP = 8 # 代码/日志通道;BASS_CH=8
|
||||||
|
DRUM_CH_DISP = 9
|
||||||
|
|
||||||
|
PITCH_RE = re.compile(
|
||||||
|
r"\[PITCH\]\s+(on|off)\s+ch(\d+)\((\w+)\)\s+"
|
||||||
|
r"(\d+)->(\d+)\s+d=(-?\d+)\s+deg=(\d+)\s+chord=(\d+)\s+"
|
||||||
|
r"xp=(\d+)\s+xf=(-?\d+)\s+fl=0x([0-9A-Fa-f]+)\s*(.*)$"
|
||||||
|
)
|
||||||
|
REG_RE = re.compile(
|
||||||
|
r"\[REG\s*\]\s+fp\s+(\d+)->(\d+)\s+chord=(\d+)\s+deg=(\d+)\s+xp=(\d+)\s+xf=(-?\d+)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PitchEvent:
|
||||||
|
is_on: bool
|
||||||
|
ch: int
|
||||||
|
role: str
|
||||||
|
key_in: int
|
||||||
|
key_out: int
|
||||||
|
delta: int
|
||||||
|
deg: int
|
||||||
|
chord: int
|
||||||
|
xp: int
|
||||||
|
xf: int
|
||||||
|
flags: int
|
||||||
|
tags: str
|
||||||
|
raw: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CheckResult:
|
||||||
|
ok: int = 0
|
||||||
|
fail: int = 0
|
||||||
|
skip: int = 0
|
||||||
|
messages: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add_ok(self, msg: str) -> None:
|
||||||
|
self.ok += 1
|
||||||
|
self.messages.append(f"OK {msg}")
|
||||||
|
|
||||||
|
def add_fail(self, msg: str) -> None:
|
||||||
|
self.fail += 1
|
||||||
|
self.messages.append(f"FAIL {msg}")
|
||||||
|
|
||||||
|
def add_skip(self, msg: str) -> None:
|
||||||
|
self.skip += 1
|
||||||
|
self.messages.append(f"SKIP {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def import_deps() -> None:
|
||||||
|
try:
|
||||||
|
import pylink # noqa: F401
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit("Missing dependency: pip install pylink-square") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def connect_jlink(device: str):
|
||||||
|
import pylink
|
||||||
|
|
||||||
|
jlink = pylink.JLink()
|
||||||
|
jlink.open()
|
||||||
|
try:
|
||||||
|
jlink.exec_command("HideDeviceSelection = 1")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
||||||
|
|
||||||
|
last_error = None
|
||||||
|
for candidate in (device, *DEFAULT_DEVICES):
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
jlink.exec_command(f"Device = {candidate}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.connect(candidate)
|
||||||
|
print(f"Connected as {candidate}", flush=True)
|
||||||
|
return jlink
|
||||||
|
except pylink.errors.JLinkException as exc:
|
||||||
|
last_error = exc
|
||||||
|
jlink.close()
|
||||||
|
raise SystemExit(f"Failed to connect J-Link: {last_error}")
|
||||||
|
|
||||||
|
|
||||||
|
def find_rtt_control_block(jlink, ram_base: int = 0x20000000, ram_size: int = 0x18000) -> int | None:
|
||||||
|
needle = b"SEGGER RTT"
|
||||||
|
chunk = 0x1000
|
||||||
|
was_halted = jlink.halted()
|
||||||
|
if not was_halted:
|
||||||
|
jlink.halt()
|
||||||
|
try:
|
||||||
|
for off in range(0, ram_size, chunk):
|
||||||
|
data = bytes(jlink.memory_read8(ram_base + off, min(chunk, ram_size - off)))
|
||||||
|
idx = data.find(needle)
|
||||||
|
if idx >= 0:
|
||||||
|
return ram_base + off + idx
|
||||||
|
finally:
|
||||||
|
if not was_halted:
|
||||||
|
if hasattr(jlink, "restart"):
|
||||||
|
jlink.restart()
|
||||||
|
else:
|
||||||
|
jlink.go()
|
||||||
|
time.sleep(0.1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def wait_rtt_ready(jlink, timeout_s: float = 15.0) -> None:
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
if jlink.rtt_get_num_up_buffers() > 0:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(0.2)
|
||||||
|
raise SystemExit("RTT control block found but buffers not ready")
|
||||||
|
|
||||||
|
|
||||||
|
def send_command(jlink, payload: bytes, retries: int = 30) -> None:
|
||||||
|
if isinstance(payload, str):
|
||||||
|
payload = payload.encode("ascii")
|
||||||
|
if not payload.endswith(b"\n"):
|
||||||
|
payload += b"\n"
|
||||||
|
for _ in range(retries):
|
||||||
|
wrote = jlink.rtt_write(0, list(payload))
|
||||||
|
if wrote > 0:
|
||||||
|
print(f">> {payload.decode('ascii', errors='replace').strip()}", flush=True)
|
||||||
|
return
|
||||||
|
time.sleep(0.2)
|
||||||
|
raise SystemExit(f"Failed to send RTT cmd: {payload!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def drain_rtt(jlink, seconds: float = 0.15) -> str:
|
||||||
|
buf = b""
|
||||||
|
deadline = time.time() + seconds
|
||||||
|
while time.time() < deadline:
|
||||||
|
chunk = jlink.rtt_read(0, 4096)
|
||||||
|
if chunk:
|
||||||
|
buf += bytes(chunk)
|
||||||
|
else:
|
||||||
|
time.sleep(0.01)
|
||||||
|
return buf.decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_pitch_line(line: str) -> PitchEvent | None:
|
||||||
|
m = PITCH_RE.search(line)
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
return PitchEvent(
|
||||||
|
is_on=m.group(1) == "on",
|
||||||
|
ch=int(m.group(2)),
|
||||||
|
role=m.group(3),
|
||||||
|
key_in=int(m.group(4)),
|
||||||
|
key_out=int(m.group(5)),
|
||||||
|
delta=int(m.group(6)),
|
||||||
|
deg=int(m.group(7)),
|
||||||
|
chord=int(m.group(8)),
|
||||||
|
xp=int(m.group(9)),
|
||||||
|
xf=int(m.group(10)),
|
||||||
|
flags=int(m.group(11), 16),
|
||||||
|
tags=m.group(12).strip(),
|
||||||
|
raw=line.strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def chord_degree(chord: int) -> int:
|
||||||
|
if chord < 1 or chord > 21:
|
||||||
|
return 0
|
||||||
|
return (chord - 1) // 3 + 1
|
||||||
|
|
||||||
|
|
||||||
|
def need_register_fix(chord: int) -> bool:
|
||||||
|
return 7 <= chord <= 21
|
||||||
|
|
||||||
|
|
||||||
|
def map_bass(key: int, xp: int) -> tuple[int, int]:
|
||||||
|
"""Return (out, flags) mirroring App_Auto_MapBassNote."""
|
||||||
|
out = key - 12
|
||||||
|
flags = 0x01
|
||||||
|
while out < MIDI_E1:
|
||||||
|
out += 12
|
||||||
|
flags |= 0x02
|
||||||
|
if xp >= XPOSE_FS:
|
||||||
|
out -= 12
|
||||||
|
flags |= 0x04
|
||||||
|
while out < MIDI_E1:
|
||||||
|
out += 12
|
||||||
|
flags |= 0x02
|
||||||
|
return max(0, min(127, out)), flags
|
||||||
|
|
||||||
|
|
||||||
|
def map_chord(key: int, xp: int) -> tuple[int, int, bool]:
|
||||||
|
"""Return (out_or_clamp_hint, flags, need_near).
|
||||||
|
|
||||||
|
When need_near is True, exact out is unknown without chord table;
|
||||||
|
caller should only require out in [E2, #G4].
|
||||||
|
"""
|
||||||
|
out = key - 12
|
||||||
|
flags = 0x01
|
||||||
|
while out < MIDI_E2:
|
||||||
|
out += 12
|
||||||
|
flags |= 0x08
|
||||||
|
need_near = out > MIDI_GSHARP4
|
||||||
|
if need_near:
|
||||||
|
flags |= 0x10
|
||||||
|
out = MIDI_GSHARP4 # placeholder; exact nearest checked soft
|
||||||
|
if xp >= XPOSE_FS:
|
||||||
|
out -= 12
|
||||||
|
flags |= 0x04
|
||||||
|
while out < MIDI_E2:
|
||||||
|
out += 12
|
||||||
|
flags |= 0x08
|
||||||
|
if out > MIDI_GSHARP4:
|
||||||
|
flags |= 0x10
|
||||||
|
need_near = True
|
||||||
|
out = MIDI_GSHARP4
|
||||||
|
return max(0, min(127, out)), flags, need_near
|
||||||
|
|
||||||
|
|
||||||
|
def expect_for_event(ev: PitchEvent) -> tuple[str, bool]:
|
||||||
|
"""Return (detail, ok)."""
|
||||||
|
if not ev.is_on:
|
||||||
|
return "note-off ignored", True
|
||||||
|
|
||||||
|
if ev.delta != ev.key_out - ev.key_in:
|
||||||
|
return f"d mismatch {ev.delta} != {ev.key_out - ev.key_in}", False
|
||||||
|
|
||||||
|
deg_exp = chord_degree(ev.chord)
|
||||||
|
if deg_exp and ev.deg != deg_exp:
|
||||||
|
return f"deg {ev.deg} != expected {deg_exp}", False
|
||||||
|
|
||||||
|
xf_exp = 1 if ev.xp >= XPOSE_FS else 0
|
||||||
|
if ev.xf != xf_exp:
|
||||||
|
return f"xf {ev.xf} != expected {xf_exp} (xp={ev.xp})", False
|
||||||
|
|
||||||
|
if ev.ch == DRUM_CH_DISP or ev.role == "drum":
|
||||||
|
return "drum should not appear in PITCH", False
|
||||||
|
|
||||||
|
fix = need_register_fix(ev.chord)
|
||||||
|
if not fix:
|
||||||
|
if ev.key_out != ev.key_in or ev.delta != 0:
|
||||||
|
return f"I/II should PASS {ev.key_in}->{ev.key_in}, got {ev.key_out}", False
|
||||||
|
if "PASS" not in ev.tags and ev.flags != 0:
|
||||||
|
# flags may be 0 on pass path
|
||||||
|
pass
|
||||||
|
return f"PASS I/II {ev.key_in}", True
|
||||||
|
|
||||||
|
if ev.ch == BASS_CH_DISP or ev.role == "bass":
|
||||||
|
exp, exp_fl = map_bass(ev.key_in, ev.xp)
|
||||||
|
if ev.key_out != exp:
|
||||||
|
return f"bass expect {ev.key_in}->{exp}, got {ev.key_out}", False
|
||||||
|
if ev.key_out < MIDI_E1:
|
||||||
|
return f"bass below E1: {ev.key_out}", False
|
||||||
|
# flag bits that must be present
|
||||||
|
if (exp_fl & 0x01) and not (ev.flags & 0x01):
|
||||||
|
return f"bass missing -12 flag fl=0x{ev.flags:02X}", False
|
||||||
|
return f"bass {ev.key_in}->{ev.key_out}", True
|
||||||
|
|
||||||
|
# chord path
|
||||||
|
exp, exp_fl, need_near = map_chord(ev.key_in, ev.xp)
|
||||||
|
if not (MIDI_E2 <= ev.key_out <= MIDI_GSHARP4):
|
||||||
|
return f"chord out {ev.key_out} not in [{MIDI_E2},{MIDI_GSHARP4}]", False
|
||||||
|
if need_near:
|
||||||
|
if not (ev.flags & 0x10) and "NEAR" not in ev.tags:
|
||||||
|
# soft: range ok is enough if nearest not flagged but clamped somehow
|
||||||
|
return f"chord NEAR expected (in={ev.key_in} out={ev.key_out})", True
|
||||||
|
return f"chord NEAR {ev.key_in}->{ev.key_out}", True
|
||||||
|
if ev.key_out != exp:
|
||||||
|
return f"chord expect {ev.key_in}->{exp}, got {ev.key_out}", False
|
||||||
|
return f"chord {ev.key_in}->{ev.key_out}", True
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_lines(lines: Iterable[str], result: CheckResult | None = None) -> CheckResult:
|
||||||
|
result = result or CheckResult()
|
||||||
|
pitch_n = 0
|
||||||
|
roles: set[str] = set()
|
||||||
|
chords: set[int] = set()
|
||||||
|
for line in lines:
|
||||||
|
line = line.rstrip("\n")
|
||||||
|
if "[REG" in line and "fp " in line:
|
||||||
|
m = REG_RE.search(line)
|
||||||
|
if m:
|
||||||
|
chord = int(m.group(3))
|
||||||
|
deg = int(m.group(4))
|
||||||
|
xp = int(m.group(5))
|
||||||
|
xf = int(m.group(6))
|
||||||
|
deg_exp = chord_degree(chord)
|
||||||
|
xf_exp = 1 if xp >= XPOSE_FS else 0
|
||||||
|
if deg_exp and deg != deg_exp:
|
||||||
|
result.add_fail(f"REG deg {deg}!={deg_exp} chord={chord}")
|
||||||
|
elif xf != xf_exp:
|
||||||
|
result.add_fail(f"REG xf {xf}!={xf_exp} xp={xp}")
|
||||||
|
else:
|
||||||
|
result.add_ok(f"REG fp chord={chord} deg={deg} xp={xp} xf={xf}")
|
||||||
|
|
||||||
|
ev = parse_pitch_line(line)
|
||||||
|
if not ev:
|
||||||
|
continue
|
||||||
|
pitch_n += 1
|
||||||
|
roles.add(ev.role)
|
||||||
|
chords.add(ev.chord)
|
||||||
|
detail, ok = expect_for_event(ev)
|
||||||
|
msg = f"ch{ev.ch}({ev.role}) chord={ev.chord} {ev.key_in}->{ev.key_out} | {detail}"
|
||||||
|
if ok:
|
||||||
|
result.add_ok(msg)
|
||||||
|
else:
|
||||||
|
result.add_fail(msg)
|
||||||
|
|
||||||
|
if pitch_n == 0:
|
||||||
|
result.add_skip("no [PITCH] lines found")
|
||||||
|
else:
|
||||||
|
if "bass" not in roles and not any(
|
||||||
|
f"ch{BASS_CH_DISP}(" in m for m in result.messages
|
||||||
|
):
|
||||||
|
result.add_skip(f"no bass PITCH on ch{BASS_CH_DISP} (style may omit bass, or old FW)")
|
||||||
|
if not any(need_register_fix(c) for c in chords):
|
||||||
|
result.add_skip("no III+ chord keys observed")
|
||||||
|
if not any(not need_register_fix(c) and 1 <= c <= 6 for c in chords):
|
||||||
|
result.add_skip("no I/II chord keys observed")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def print_report(result: CheckResult, out_path: str | None = None) -> int:
|
||||||
|
print("\n======== PITCH REG TEST REPORT ========", flush=True)
|
||||||
|
for msg in result.messages:
|
||||||
|
# only print fails + summary skips loudly; OK compacted
|
||||||
|
if msg.startswith("FAIL") or msg.startswith("SKIP"):
|
||||||
|
print(msg, flush=True)
|
||||||
|
ok_short = [m for m in result.messages if m.startswith("OK")]
|
||||||
|
print(f"OK={result.ok} FAIL={result.fail} SKIP={result.skip}", flush=True)
|
||||||
|
if ok_short:
|
||||||
|
print(f"(first OK samples: {len(ok_short)} total)", flush=True)
|
||||||
|
for m in ok_short[:8]:
|
||||||
|
print(m, flush=True)
|
||||||
|
if len(ok_short) > 8:
|
||||||
|
print(f"... +{len(ok_short) - 8} more OK", flush=True)
|
||||||
|
if out_path:
|
||||||
|
with open(out_path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(f"# pitch reg report {datetime.now().isoformat(timespec='seconds')}\n")
|
||||||
|
f.write(f"OK={result.ok} FAIL={result.fail} SKIP={result.skip}\n")
|
||||||
|
for m in result.messages:
|
||||||
|
f.write(m + "\n")
|
||||||
|
print(f"Report saved: {os.path.abspath(out_path)}", flush=True)
|
||||||
|
return 0 if result.fail == 0 and result.ok > 0 else 1
|
||||||
|
|
||||||
|
|
||||||
|
class RttSession:
|
||||||
|
def __init__(self, jlink):
|
||||||
|
self.jlink = jlink
|
||||||
|
self.buf = b""
|
||||||
|
self.lines: list[str] = []
|
||||||
|
|
||||||
|
def cmd(self, text: str, settle: float = 0.2) -> None:
|
||||||
|
send_command(self.jlink, text.encode("ascii"))
|
||||||
|
self.pump(settle)
|
||||||
|
|
||||||
|
def pump(self, seconds: float) -> list[str]:
|
||||||
|
new_lines: list[str] = []
|
||||||
|
deadline = time.time() + seconds
|
||||||
|
while time.time() < deadline:
|
||||||
|
chunk = self.jlink.rtt_read(0, 4096)
|
||||||
|
if chunk:
|
||||||
|
self.buf += bytes(chunk)
|
||||||
|
while b"\n" in self.buf:
|
||||||
|
raw, self.buf = self.buf.split(b"\n", 1)
|
||||||
|
line = raw.decode("utf-8", errors="replace").rstrip("\r")
|
||||||
|
if line:
|
||||||
|
self.lines.append(line)
|
||||||
|
new_lines.append(line)
|
||||||
|
if (
|
||||||
|
"[PITCH]" in line
|
||||||
|
or "[REG" in line
|
||||||
|
or "[KEY" in line
|
||||||
|
or "CHORD_" in line
|
||||||
|
or "TONE_" in line
|
||||||
|
or "TM1617_" in line
|
||||||
|
or "ADC_KEY_" in line
|
||||||
|
or "LOG_STATUS" in line
|
||||||
|
or "TAP inject" in line
|
||||||
|
or "UI_" in line
|
||||||
|
):
|
||||||
|
print(line, flush=True)
|
||||||
|
else:
|
||||||
|
time.sleep(0.01)
|
||||||
|
return new_lines
|
||||||
|
|
||||||
|
def cmd_ack(self, text: str, ack: str, timeout_s: float = 3.0, retries: int = 6) -> bool:
|
||||||
|
"""Send command until ack substring appears (handles RTT down loss under load)."""
|
||||||
|
for attempt in range(1, retries + 1):
|
||||||
|
# brief quiet drain then send
|
||||||
|
self.pump(0.15)
|
||||||
|
send_command(self.jlink, text.encode("ascii"))
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
while time.time() < deadline:
|
||||||
|
for line in self.pump(0.1):
|
||||||
|
if ack in line:
|
||||||
|
return True
|
||||||
|
print(f"!! no ack '{ack}' for '{text}' (try {attempt}/{retries})", flush=True)
|
||||||
|
time.sleep(0.25)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop_band(self) -> None:
|
||||||
|
self.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=4)
|
||||||
|
self.pump(0.4)
|
||||||
|
|
||||||
|
def set_xpose(self, xp: int) -> bool:
|
||||||
|
return self.cmd_ack(f"chord xpose {xp}", "CHORD_XPOSE_OK", timeout_s=2.0, retries=5)
|
||||||
|
|
||||||
|
def set_key(self, key: int) -> bool:
|
||||||
|
return self.cmd_ack(f"chord key {key}", "CHORD_KEY_OK", timeout_s=2.0, retries=5)
|
||||||
|
|
||||||
|
def phase_switch(self, name: str, key: int, xpose: int | None, hold_s: float) -> None:
|
||||||
|
"""Switch chord/xpose while accompaniment is already running.
|
||||||
|
Note: plain `tone pick` clears KEY_ID to 1 — do not call it after inject.
|
||||||
|
"""
|
||||||
|
print(f"\n=== {name}: key={key} xpose={xpose} ===", flush=True)
|
||||||
|
if xpose is not None:
|
||||||
|
if not self.set_xpose(xpose):
|
||||||
|
print(f"FAIL setup xpose={xpose}", flush=True)
|
||||||
|
self.set_key(0)
|
||||||
|
if not self.set_key(key):
|
||||||
|
print(f"FAIL setup key={key}", flush=True)
|
||||||
|
self.pump(hold_s)
|
||||||
|
|
||||||
|
|
||||||
|
def run_auto(device: str, hold_s: float, report: str | None, log_path: str | None) -> int:
|
||||||
|
import_deps()
|
||||||
|
jlink = connect_jlink(device)
|
||||||
|
try:
|
||||||
|
cb = find_rtt_control_block(jlink)
|
||||||
|
if cb is None:
|
||||||
|
raise SystemExit("SEGGER RTT CB not found")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
wait_rtt_ready(jlink)
|
||||||
|
sess = RttSession(jlink)
|
||||||
|
sess.pump(0.3)
|
||||||
|
|
||||||
|
sess.cmd("log clear", 0.3)
|
||||||
|
sess.cmd("tone local", 1.0)
|
||||||
|
# Start band first (pick forces chord=1), then inject keys while playing
|
||||||
|
if not sess.set_xpose(2):
|
||||||
|
print("FAIL initial xpose=2", flush=True)
|
||||||
|
sess.cmd("tone pick", settle=0.8)
|
||||||
|
print("\n=== I (default after pick) ===", flush=True)
|
||||||
|
sess.pump(hold_s)
|
||||||
|
|
||||||
|
sess.phase_switch("I/II key2 PASS", key=2, xpose=None, hold_s=hold_s)
|
||||||
|
sess.phase_switch("III key8 -12", key=8, xpose=None, hold_s=hold_s)
|
||||||
|
sess.phase_switch("VII key20 -12", key=20, xpose=None, hold_s=hold_s)
|
||||||
|
sess.phase_switch("VII key20 xpose=#F", key=20, xpose=6, hold_s=hold_s)
|
||||||
|
|
||||||
|
print("\n=== stop ===", flush=True)
|
||||||
|
sess.stop_band()
|
||||||
|
sess.pump(0.4)
|
||||||
|
|
||||||
|
if log_path:
|
||||||
|
with open(log_path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(f"# auto pitch test {datetime.now().isoformat(timespec='seconds')}\n")
|
||||||
|
for line in sess.lines:
|
||||||
|
f.write(line + "\n")
|
||||||
|
print(f"Log saved: {os.path.abspath(log_path)}", flush=True)
|
||||||
|
|
||||||
|
joined = "\n".join(sess.lines)
|
||||||
|
if "CHORD_KEY_OK" not in joined and "CHORD_KEY_BAD" not in joined:
|
||||||
|
print(
|
||||||
|
"WARNING: no CHORD_KEY_OK — firmware may lack 'chord key' RTT cmd. "
|
||||||
|
"Rebuild/flash, or use --listen / --analyze.",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = analyze_lines(sess.lines)
|
||||||
|
has_pass = any("PASS I/II" in m for m in result.messages)
|
||||||
|
has_iii_raw = any(
|
||||||
|
"[PITCH]" in line and ("chord=8" in line or "chord=20" in line or "deg=3" in line or "deg=7" in line)
|
||||||
|
for line in sess.lines
|
||||||
|
)
|
||||||
|
has_iii_ok = any(
|
||||||
|
m.startswith("OK") and "PASS" not in m and "(chord)" in m
|
||||||
|
for m in result.messages
|
||||||
|
)
|
||||||
|
has_xf = any("[PITCH]" in line and "xf=1" in line and "deg=7" in line for line in sess.lines)
|
||||||
|
if not has_pass:
|
||||||
|
result.add_fail("coverage: no I/II PASS samples")
|
||||||
|
if not has_iii_raw:
|
||||||
|
result.add_fail("coverage: no III+/VII chord in PITCH")
|
||||||
|
elif not has_iii_ok:
|
||||||
|
result.add_fail("coverage: III+ present but mapping asserts failed")
|
||||||
|
if not has_xf:
|
||||||
|
result.add_skip("coverage: no VII+xf=1 samples")
|
||||||
|
|
||||||
|
return print_report(result, report)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
jlink.rtt_stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.close()
|
||||||
|
|
||||||
|
|
||||||
|
def run_listen(device: str, seconds: float, report: str | None, log_path: str | None) -> int:
|
||||||
|
import_deps()
|
||||||
|
jlink = connect_jlink(device)
|
||||||
|
try:
|
||||||
|
cb = find_rtt_control_block(jlink)
|
||||||
|
if cb is None:
|
||||||
|
raise SystemExit("SEGGER RTT CB not found")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
wait_rtt_ready(jlink)
|
||||||
|
sess = RttSession(jlink)
|
||||||
|
sess.pump(0.2)
|
||||||
|
sess.cmd("log clear", 0.2)
|
||||||
|
print(
|
||||||
|
f"Listening {seconds:.0f}s — press I/II then III+ chords; set transpose>=#F if possible.",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
sess.pump(seconds)
|
||||||
|
if log_path:
|
||||||
|
with open(log_path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(f"# listen pitch test {datetime.now().isoformat(timespec='seconds')}\n")
|
||||||
|
for line in sess.lines:
|
||||||
|
f.write(line + "\n")
|
||||||
|
print(f"Log saved: {os.path.abspath(log_path)}", flush=True)
|
||||||
|
result = analyze_lines(sess.lines)
|
||||||
|
return print_report(result, report)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
jlink.rtt_stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.close()
|
||||||
|
|
||||||
|
|
||||||
|
def run_analyze(path: str, report: str | None) -> int:
|
||||||
|
with open(path, encoding="utf-8", errors="replace") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
result = analyze_lines(lines)
|
||||||
|
return print_report(result, report)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="K1 pitch/register RTT auto-test")
|
||||||
|
parser.add_argument("--device", default="AT32F403AC")
|
||||||
|
parser.add_argument("--hold", type=float, default=2.5, help="Seconds to hold each injected key")
|
||||||
|
parser.add_argument("--seconds", type=float, default=45.0, help="Listen duration")
|
||||||
|
parser.add_argument("--listen", action="store_true", help="Do not inject; monitor only")
|
||||||
|
parser.add_argument("--analyze", metavar="LOG", help="Offline analyze a dump log")
|
||||||
|
parser.add_argument("--out-log", default="", help="Save captured RTT lines")
|
||||||
|
parser.add_argument("--report", default="", help="Save pass/fail report")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
report = args.report or None
|
||||||
|
log_path = args.out_log or None
|
||||||
|
|
||||||
|
if args.analyze:
|
||||||
|
raise SystemExit(run_analyze(args.analyze, report))
|
||||||
|
if args.listen:
|
||||||
|
raise SystemExit(run_listen(args.device, args.seconds, report, log_path))
|
||||||
|
raise SystemExit(run_auto(args.device, args.hold, report, log_path))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,447 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_protocol_app_sim.py — 模拟手机 App 对吉他固件做全协议串口测试
|
||||||
|
|
||||||
|
协议来源: Doc/指令测试.docx (= 指令测试0907.docx)
|
||||||
|
通道: UART4 (BLE 桥), 115200 8N1; 帧格式 F0 60 <cmd1> <cmd2> [data...] F7
|
||||||
|
设备主动上报为 F0 51 ... F7 (测试时跳过, 不计为响应)
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python test_protocol_app_sim.py --port COM5 # 跑全部安全用例
|
||||||
|
python test_protocol_app_sim.py --port COM5 --allow-poweroff# 含 05 00 关机用例
|
||||||
|
python test_protocol_app_sim.py --selftest # 无硬件自检(帧编解码)
|
||||||
|
|
||||||
|
依赖: pyserial (pip install pyserial)
|
||||||
|
退出码: 0 = 全部通过, 1 = 有 FAIL
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
try:
|
||||||
|
import serial
|
||||||
|
except ImportError:
|
||||||
|
serial = None # --selftest 不需要 pyserial
|
||||||
|
|
||||||
|
HEAD, TAIL, APP_ID, DEV_ID = 0xF0, 0xF7, 0x60, 0x51
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- frame codec
|
||||||
|
def build(*payload):
|
||||||
|
"""App -> 设备: F0 60 <payload...> F7"""
|
||||||
|
return bytes([HEAD, APP_ID, *payload, TAIL])
|
||||||
|
|
||||||
|
|
||||||
|
class FrameParser:
|
||||||
|
"""字节流 -> 完整帧; 只收 F0...F7, 其余丢弃"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.buf = bytearray()
|
||||||
|
|
||||||
|
def feed(self, data: bytes):
|
||||||
|
"""返回本轮解析出的完整帧列表"""
|
||||||
|
out = []
|
||||||
|
for b in data:
|
||||||
|
if not self.buf:
|
||||||
|
if b == HEAD:
|
||||||
|
self.buf.append(b)
|
||||||
|
continue
|
||||||
|
# BLE-MIDI 粘包时可能再次出现 F0;重新同步,避免 F0 F0 60…
|
||||||
|
if b == HEAD:
|
||||||
|
self.buf = bytearray([HEAD])
|
||||||
|
continue
|
||||||
|
self.buf.append(b)
|
||||||
|
if b == TAIL or len(self.buf) >= 64:
|
||||||
|
if b == TAIL and len(self.buf) >= 4:
|
||||||
|
out.append(bytes(self.buf))
|
||||||
|
self.buf.clear()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def hexs(b):
|
||||||
|
return " ".join(f"{x:02X}" for x in b) if b else "(none)"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- serial wrap
|
||||||
|
class AppSim:
|
||||||
|
def __init__(self, port, baud=115200, timeout=0.15):
|
||||||
|
self.ser = serial.Serial(port, baud, bytesize=8, parity="N",
|
||||||
|
stopbits=1, timeout=timeout)
|
||||||
|
self.parser = FrameParser()
|
||||||
|
self.pending = []
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.ser.close()
|
||||||
|
|
||||||
|
def _pump(self):
|
||||||
|
data = self.ser.read(256)
|
||||||
|
if data:
|
||||||
|
self.pending.extend(self.parser.feed(data))
|
||||||
|
|
||||||
|
def send(self, frame: bytes):
|
||||||
|
self.ser.write(frame)
|
||||||
|
self.ser.flush()
|
||||||
|
|
||||||
|
def read_frame(self, timeout=1.0, want_dev=False):
|
||||||
|
"""读一帧; 默认跳过设备主动上报(0x51), want_dev=True 时只要 0x51"""
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
while True:
|
||||||
|
self._pump()
|
||||||
|
for i, f in enumerate(self.pending):
|
||||||
|
is_dev = len(f) > 1 and f[1] == DEV_ID
|
||||||
|
if is_dev == want_dev:
|
||||||
|
return self.pending.pop(i)
|
||||||
|
self.pending.clear() # 丢掉方向不符的帧
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
return None
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
def drain(self, quiet=0.2):
|
||||||
|
while self.read_frame(timeout=quiet) is not None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def query(self, frame: bytes, timeout=1.0):
|
||||||
|
self.drain()
|
||||||
|
self.send(frame)
|
||||||
|
return self.read_frame(timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- test engine
|
||||||
|
class Results:
|
||||||
|
def __init__(self):
|
||||||
|
self.rows = []
|
||||||
|
|
||||||
|
def add(self, name, status, detail=""):
|
||||||
|
self.rows.append((name, status, detail))
|
||||||
|
print(f"[{status:>4}] {name}" + (f" | {detail}" if detail else ""))
|
||||||
|
|
||||||
|
def summary(self):
|
||||||
|
n = {s: sum(1 for r in self.rows if r[1] == s)
|
||||||
|
for s in ("PASS", "FAIL", "SKIP", "SENT")}
|
||||||
|
print("\n===== 汇总 =====")
|
||||||
|
for k in ("PASS", "FAIL", "SKIP", "SENT"):
|
||||||
|
print(f"{k:>5}: {n[k]}")
|
||||||
|
for name, status, detail in self.rows:
|
||||||
|
if status == "FAIL":
|
||||||
|
print(f" FAIL: {name} | {detail}")
|
||||||
|
return 1 if n["FAIL"] else 0
|
||||||
|
|
||||||
|
|
||||||
|
def expect(resp, prefix, length=None):
|
||||||
|
"""校验响应帧头/命令字/长度"""
|
||||||
|
if resp is None:
|
||||||
|
return False, "timeout, no reply"
|
||||||
|
if len(resp) < 5 or resp[0] != HEAD or resp[-1] != TAIL:
|
||||||
|
return False, f"bad frame: {hexs(resp)}"
|
||||||
|
if tuple(resp[1:1 + len(prefix)]) != tuple(prefix):
|
||||||
|
return False, f"prefix mismatch: {hexs(resp)}"
|
||||||
|
if length is not None and len(resp) != length:
|
||||||
|
return False, f"len {len(resp)} != {length}: {hexs(resp)}"
|
||||||
|
return True, hexs(resp)
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_ver4(payload: bytes, *, kind: str = "semver") -> str:
|
||||||
|
"""解码版本字节为可读串。
|
||||||
|
|
||||||
|
kind:
|
||||||
|
semver — 前 3 字节 major.minor.patch(01 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):
|
||||||
|
R = Results()
|
||||||
|
|
||||||
|
# ---------------- 1. 设备信息 0x01 ----------------
|
||||||
|
r = sim.query(build(0x01, 0x01))
|
||||||
|
ok, d = expect(r, (0x60, 0x01, 0x01), 6)
|
||||||
|
R.add("01 01 连接设备", "PASS" if ok else "FAIL", d)
|
||||||
|
|
||||||
|
r = sim.query(build(0x01, 0x02))
|
||||||
|
ok, d = expect(r, (0x60, 0x01, 0x02))
|
||||||
|
if ok:
|
||||||
|
name = bytes(r[4:-1])
|
||||||
|
ok = all(0x20 <= c < 0x7F for c in name)
|
||||||
|
d += f" name={name!r}"
|
||||||
|
R.add("01 02 设备名", "PASS" if ok else "FAIL", d)
|
||||||
|
|
||||||
|
for sub, name, kind in (
|
||||||
|
(0x03, "固件主版本", "semver"),
|
||||||
|
(0x04, "音源版本", "date"),
|
||||||
|
(0x05, "UI版本", "date"),
|
||||||
|
):
|
||||||
|
r = sim.query(build(0x01, sub))
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
ok, d = expect(r, (0x60, 0x01, 0x06), 13)
|
||||||
|
R.add("01 06 其他信息", "PASS" if ok else "FAIL", d)
|
||||||
|
|
||||||
|
r = sim.query(build(0x01, 0x07))
|
||||||
|
ok, d = expect(r, (0x60, 0x01, 0x07), 29) # 12B UID → 24 hex ASCII
|
||||||
|
if ok:
|
||||||
|
hx = bytes(r[4:-1])
|
||||||
|
ok = len(hx) == 24 and all(c in b"0123456789ABCDEF" for c in hx)
|
||||||
|
d += f" uid_hex={hx.decode('ascii', 'replace')}"
|
||||||
|
R.add("01 07 设备编码(UID)", "PASS" if ok else "FAIL", d)
|
||||||
|
|
||||||
|
# 01 0F:BLE-MIDI 安全子命令(原 01 FF 的 0xFF 非法出现在 SysEx 数据中)
|
||||||
|
r = sim.query(build(0x01, 0x0F))
|
||||||
|
ok, d = expect(r, (0x60, 0x01, 0x0F))
|
||||||
|
if ok:
|
||||||
|
code = bytes(r[4:-1])
|
||||||
|
ok = len(code) > 0 and all(0x20 <= c < 0x7F for c in code)
|
||||||
|
d += f" code={code!r}"
|
||||||
|
R.add("01 0F 固件编码", "PASS" if ok else "FAIL", d)
|
||||||
|
|
||||||
|
# 自动关机: 读 -> 设15 -> 读验证 -> 恢复原值
|
||||||
|
r0 = sim.query(build(0x01, 0x0A))
|
||||||
|
ok, d = expect(r0, (0x60, 0x01, 0x0A), 6)
|
||||||
|
R.add("01 0A 自动关机(读)", "PASS" if ok else "FAIL", d)
|
||||||
|
if ok:
|
||||||
|
orig = r0[4]
|
||||||
|
r = sim.query(build(0x01, 0x11, 15))
|
||||||
|
ok, d = expect(r, (0x60, 0x01, 0x11, 15), 6)
|
||||||
|
R.add("01 11 自动关机(设15)", "PASS" if ok else "FAIL", d)
|
||||||
|
r = sim.query(build(0x01, 0x0A))
|
||||||
|
ok, d = expect(r, (0x60, 0x01, 0x0A, 15), 6)
|
||||||
|
R.add("01 0A 回读=15", "PASS" if ok else "FAIL", d)
|
||||||
|
sim.query(build(0x01, 0x11, orig)) # restore
|
||||||
|
|
||||||
|
r = sim.query(build(0x01, 0x00))
|
||||||
|
ok, d = expect(r, (0x60, 0x01, 0x00), 6)
|
||||||
|
R.add("01 00 断开设备", "PASS" if ok else "FAIL", d)
|
||||||
|
sim.query(build(0x01, 0x01)) # 重新连接, 便于后续用例
|
||||||
|
|
||||||
|
# ---------------- 2. 和弦映射 0x02 ----------------
|
||||||
|
r = sim.query(build(0x02, 0x01))
|
||||||
|
ok, d = expect(r, (0x60, 0x02, 0x01), 47)
|
||||||
|
R.add("02 01 读和弦映射表", "PASS" if ok else "FAIL", d)
|
||||||
|
cur_map = bytes(r[4:-1]) if ok else None # 21B pitch + 21B chord
|
||||||
|
|
||||||
|
if cur_map:
|
||||||
|
key = 1
|
||||||
|
orig_pitch, orig_chord = cur_map[key - 1], cur_map[21 + key - 1]
|
||||||
|
|
||||||
|
sim.send(build(0x02, 0x02, key - 1, 0x02)) # pitch: 升位
|
||||||
|
time.sleep(0.1)
|
||||||
|
r = sim.query(build(0x02, 0x01))
|
||||||
|
ok = r is not None and len(r) == 47 and r[3] == 0x01 and r[4 + key - 1] == 0x02
|
||||||
|
R.add("02 02 Pitch偏移(写+读回)", "PASS" if ok else "FAIL", hexs(r))
|
||||||
|
sim.send(build(0x02, 0x02, key - 1, orig_pitch)) # restore
|
||||||
|
|
||||||
|
sim.send(build(0x02, 0x03, key - 1, 0x08)) # chord: 小三
|
||||||
|
time.sleep(0.1)
|
||||||
|
r = sim.query(build(0x02, 0x01))
|
||||||
|
ok = r is not None and len(r) == 47 and r[4 + 21 + key - 1] == 0x08
|
||||||
|
R.add("02 03 Chord偏移(写+读回)", "PASS" if ok else "FAIL", hexs(r))
|
||||||
|
sim.send(build(0x02, 0x03, key - 1, orig_chord)) # restore
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# 02 04 整表写入(回写当前表) -> 设备应回 02 01 格式整表
|
||||||
|
r = sim.query(build(0x02, 0x04, *cur_map), timeout=2.0)
|
||||||
|
ok, d = expect(r, (0x60, 0x02, 0x01), 47)
|
||||||
|
R.add("02 04 整表写入(回读帧)", "PASS" if ok else "FAIL", d)
|
||||||
|
r2 = sim.query(build(0x02, 0x01))
|
||||||
|
ok = r2 is not None and bytes(r2[4:-1]) == cur_map
|
||||||
|
R.add("02 04 写入后整表一致", "PASS" if ok else "FAIL", hexs(r2))
|
||||||
|
else:
|
||||||
|
for n in ("02 02 Pitch偏移", "02 03 Chord偏移", "02 04 整表写入"):
|
||||||
|
R.add(n, "SKIP", "读映射表失败, 级联跳过")
|
||||||
|
|
||||||
|
# ---------------- 3. 吉他参数 0x03 ----------------
|
||||||
|
r = sim.query(build(0x03, 0x04, 0x00))
|
||||||
|
ok, d = expect(r, (0x60, 0x03, 0x04, 0x00), 9)
|
||||||
|
R.add("03 04 读当前节奏风格", "PASS" if ok else "FAIL", d)
|
||||||
|
saved_style = bytes(r[5:8]) if ok else None
|
||||||
|
|
||||||
|
# 开机特例: F0 60 03 04 01 00 00 00 F7 -> F0 60 03 04 01 <bpm> F7 + F0 51 03 <hi> <lo> F7
|
||||||
|
r = sim.query(build(0x03, 0x04, 0x01, 0x00, 0x00, 0x00))
|
||||||
|
ok, d = expect(r, (0x60, 0x03, 0x04, 0x01), 7)
|
||||||
|
R.add("03 04 开机特例(读风格#0)", "PASS" if ok else "FAIL", d)
|
||||||
|
r51 = sim.read_frame(timeout=1.0, want_dev=True)
|
||||||
|
ok51 = (r51 is not None and len(r51) == 6
|
||||||
|
and tuple(r51[:3]) == (HEAD, DEV_ID, 0x03))
|
||||||
|
R.add("03 04 开机特例(F0 51 BPM上报)",
|
||||||
|
"PASS" if ok51 else "FAIL", hexs(r51))
|
||||||
|
|
||||||
|
r = sim.query(build(0x03, 0x04, 0x01, 0x01, 0x00, 0x00))
|
||||||
|
ok, d = expect(r, (0x60, 0x03, 0x04, 0x01), 7)
|
||||||
|
R.add("03 04 设用户风格#0", "PASS" if ok else "FAIL", d)
|
||||||
|
|
||||||
|
# 用户风格分页: 回复 F0 60 03 04 02 <cntHi> <cntLo> [<idHi> <idLo> <code4B> ...] F7
|
||||||
|
r = sim.query(build(0x03, 0x04, 0x02, 0x00, 0x00, 0x00, 0x07))
|
||||||
|
ok, d = expect(r, (0x60, 0x03, 0x04, 0x02))
|
||||||
|
if ok:
|
||||||
|
cnt = r[5] * 128 + r[6]
|
||||||
|
ok = len(r) == 7 + cnt * 6 + 1
|
||||||
|
d += f" cnt={cnt}"
|
||||||
|
R.add("03 04 用户风格分页", "PASS" if ok else "FAIL", d)
|
||||||
|
|
||||||
|
# 用户风格总数: 回复 F0 60 03 04 00 <total 3B BE> F7
|
||||||
|
r = sim.query(build(0x03, 0x04, 0x03))
|
||||||
|
ok, d = expect(r, (0x60, 0x03, 0x04, 0x00), 9)
|
||||||
|
if ok:
|
||||||
|
d += f" total={(r[5] << 16) | (r[6] << 8) | r[7]}"
|
||||||
|
R.add("03 04 用户风格总数", "PASS" if ok else "FAIL", d)
|
||||||
|
|
||||||
|
if saved_style:
|
||||||
|
sim.query(build(0x03, 0x04, 0x01, *saved_style)) # restore style
|
||||||
|
|
||||||
|
# 弦音色: 读 -> 写 -> 读 -> 恢复
|
||||||
|
r0 = sim.query(build(0x03, 0x05, 0x00))
|
||||||
|
ok, d = expect(r0, (0x60, 0x03, 0x05, 0x00), 8)
|
||||||
|
R.add("03 05 读弦音色", "PASS" if ok else "FAIL", d)
|
||||||
|
if ok:
|
||||||
|
orig = r0[6]
|
||||||
|
new = (orig + 1) % 5
|
||||||
|
sim.send(build(0x03, 0x05, 0x01, 0x00, new))
|
||||||
|
time.sleep(0.1)
|
||||||
|
r = sim.query(build(0x03, 0x05, 0x00))
|
||||||
|
ok = r is not None and len(r) == 8 and r[6] == new
|
||||||
|
R.add("03 05 写弦音色(写+读回)", "PASS" if ok else "FAIL", hexs(r))
|
||||||
|
sim.send(build(0x03, 0x05, 0x01, 0x00, orig)) # restore
|
||||||
|
|
||||||
|
# BPM: 读 -> 写120 -> 读 -> 恢复
|
||||||
|
r0 = sim.query(build(0x03, 0x06, 0x00))
|
||||||
|
ok, d = expect(r0, (0x60, 0x03, 0x06, 0x00), 8)
|
||||||
|
R.add("03 06 读BPM", "PASS" if ok else "FAIL", d)
|
||||||
|
if ok:
|
||||||
|
orig = r0[5] * 128 + r0[6]
|
||||||
|
sim.send(build(0x03, 0x06, 0x01, 120 // 128, 120 % 128))
|
||||||
|
time.sleep(0.1)
|
||||||
|
r = sim.query(build(0x03, 0x06, 0x00))
|
||||||
|
ok = r is not None and len(r) == 8 and (r[5] * 128 + r[6]) == 120
|
||||||
|
R.add("03 06 写BPM=120(写+读回)", "PASS" if ok else "FAIL", hexs(r))
|
||||||
|
sim.send(build(0x03, 0x06, 0x01, orig // 128, orig % 128)) # restore
|
||||||
|
|
||||||
|
# 移调: 读 -> 写 -> 读 -> 恢复 (doc: F0 60 03 07 00 <val> F7, 7 bytes)
|
||||||
|
r0 = sim.query(build(0x03, 0x07, 0x00))
|
||||||
|
ok, d = expect(r0, (0x60, 0x03, 0x07, 0x00), 7)
|
||||||
|
R.add("03 07 读移调", "PASS" if ok else "FAIL", d)
|
||||||
|
if ok:
|
||||||
|
orig = r0[5]
|
||||||
|
new = (orig + 1) % 12
|
||||||
|
sim.send(build(0x03, 0x07, 0x01, new))
|
||||||
|
time.sleep(0.1)
|
||||||
|
r = sim.query(build(0x03, 0x07, 0x00))
|
||||||
|
ok = r is not None and len(r) == 7 and r[5] == new
|
||||||
|
R.add("03 07 写移调(写+读回)", "PASS" if ok else "FAIL", hexs(r))
|
||||||
|
sim.send(build(0x03, 0x07, 0x01, orig)) # restore
|
||||||
|
|
||||||
|
# ---------------- 4. LED / 播放控制 0x04 (无应答, 仅下发) ----------------
|
||||||
|
for i in range(7):
|
||||||
|
sim.drain()
|
||||||
|
sim.send(build(0x04, i, 0x02, 0x02))
|
||||||
|
r = sim.read_frame(timeout=0.3)
|
||||||
|
R.add(f"04 0{i} 点亮LED{i + 1}", "SENT" if r is None else "PASS",
|
||||||
|
"" if r is None else f"unexpected reply {hexs(r)}")
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
sim.drain()
|
||||||
|
sim.send(build(0x04, 0x07, 0x00, 0x00))
|
||||||
|
R.add("04 07 End/结束播放", "SENT", "无应答属正常")
|
||||||
|
|
||||||
|
# ---------------- 6. 段落跳转 0x06 (无应答, 仅下发) ----------------
|
||||||
|
for sub, name in ((0x01, "前奏"), (0x02, "间奏"), (0x03, "尾奏"),
|
||||||
|
(0x05, "A段"), (0x06, "B段"), (0x07, "C段"), (0x08, "D段")):
|
||||||
|
sim.drain()
|
||||||
|
sim.send(build(0x06, sub, 0x00))
|
||||||
|
R.add(f"06 {sub:02X} {name}", "SENT", "无应答属正常")
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
# ---------------- 5. 复位/关机 0x05 ----------------
|
||||||
|
if allow_poweroff:
|
||||||
|
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)
|
||||||
|
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 + " (设备将软关机)")
|
||||||
|
else:
|
||||||
|
R.add("05 00 复位/关机", "SKIP", "需 --allow-poweroff")
|
||||||
|
|
||||||
|
return R
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- selftest
|
||||||
|
def selftest():
|
||||||
|
R = Results()
|
||||||
|
f = build(0x03, 0x06, 0x01, 0x00, 0x40)
|
||||||
|
R.add("build frame", "PASS" if f == bytes.fromhex("F060030601004 0F7".replace(" ", "")) else "FAIL", hexs(f))
|
||||||
|
|
||||||
|
p = FrameParser()
|
||||||
|
out = p.feed(bytes.fromhex("AA F0 60 01 01")) + p.feed(bytes.fromhex("01 F7"))
|
||||||
|
R.add("parser chunked", "PASS" if out == [bytes.fromhex("F0600101 01F7".replace(" ", ""))] else "FAIL",
|
||||||
|
str([hexs(x) for x in out]))
|
||||||
|
|
||||||
|
p = FrameParser()
|
||||||
|
out = p.feed(bytes.fromhex("F0 51 05 0A F7 F0 60 01 01 01 F7".replace(" ", "")))
|
||||||
|
R.add("parser two frames", "PASS" if len(out) == 2 else "FAIL", str(len(out)))
|
||||||
|
return R.summary()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="模拟手机App的吉他协议全量测试")
|
||||||
|
ap.add_argument("--port", help="串口, 如 COM5 (UART4 115200 8N1)")
|
||||||
|
ap.add_argument("--baud", type=int, default=115200)
|
||||||
|
ap.add_argument("--allow-poweroff", action="store_true",
|
||||||
|
help="允许执行 05 00 关机用例")
|
||||||
|
ap.add_argument("--selftest", action="store_true", help="无硬件自检")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if args.selftest:
|
||||||
|
sys.exit(selftest())
|
||||||
|
if not args.port:
|
||||||
|
ap.error("需要 --port (或用 --selftest)")
|
||||||
|
if serial is None:
|
||||||
|
sys.exit("缺少 pyserial: pip install pyserial")
|
||||||
|
|
||||||
|
sim = AppSim(args.port, args.baud)
|
||||||
|
print(f"=== 协议测试开始 {args.port}@{args.baud} ===")
|
||||||
|
try:
|
||||||
|
code = run_tests(sim, allow_poweroff=args.allow_poweroff).summary()
|
||||||
|
finally:
|
||||||
|
sim.close()
|
||||||
|
sys.exit(code)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,502 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
test_protocol_ble.py — 通过笔记本蓝牙对吉他做全协议测试并出报告
|
||||||
|
|
||||||
|
协议来源: Doc/指令测试.docx
|
||||||
|
链路: 笔记本 BLE <-> "Smart Guitar MIDI" <-> 吉他 UART4
|
||||||
|
App->设备: F0 60 ... F7 ; 设备主动上报: F0 51 ... F7
|
||||||
|
|
||||||
|
GATT:
|
||||||
|
BLE-MIDI 03B80E5A-... / 7772E5DB-... (framed SysEx)
|
||||||
|
自定义串口 e49a25f8-... / e49a25e0(写) + e49a28e1(通知) (raw SysEx)
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python test_protocol_ble.py
|
||||||
|
python test_protocol_ble.py --transport midi
|
||||||
|
python test_protocol_ble.py --transport uart
|
||||||
|
python test_protocol_ble.py --unpair
|
||||||
|
python test_protocol_ble.py --smoke
|
||||||
|
python test_protocol_ble.py --selftest
|
||||||
|
|
||||||
|
依赖: bleak (pip install bleak)
|
||||||
|
报告: Doc/reports/ble_sysex_<时间戳>.md
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from test_protocol_app_sim import ( # noqa: E402
|
||||||
|
HEAD, DEV_ID, build, FrameParser, Results, run_tests, hexs)
|
||||||
|
|
||||||
|
MIDI_SERVICE = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"
|
||||||
|
MIDI_CHAR = "7772E5DB-3868-4112-A1A9-F2669D106BF3"
|
||||||
|
UART_SERVICE = "e49a25f8-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||||
|
UART_WRITE = "e49a25e0-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||||
|
UART_NOTIFY = "e49a28e1-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||||
|
DEFAULT_NAME = "Smart Guitar MIDI"
|
||||||
|
DEFAULT_MTU_PAYLOAD = 20
|
||||||
|
|
||||||
|
|
||||||
|
def ble_midi_encode_sysex(frame: bytes, max_payload: int):
|
||||||
|
ts_hi, ts_lo = 0x80, 0x80
|
||||||
|
pkts = []
|
||||||
|
cap = max_payload - 2
|
||||||
|
first = frame[:cap]
|
||||||
|
pkts.append(bytes([ts_hi, ts_lo]) + first)
|
||||||
|
rest = frame[len(first):]
|
||||||
|
while len(rest) > max_payload - 2:
|
||||||
|
pkts.append(bytes([ts_hi]) + rest[: max_payload - 1])
|
||||||
|
rest = rest[max_payload - 1 :]
|
||||||
|
if rest:
|
||||||
|
pkts.append(bytes([ts_hi, ts_lo]) + rest)
|
||||||
|
return pkts
|
||||||
|
|
||||||
|
|
||||||
|
def ble_midi_decode_packet(payload: bytes) -> bytes:
|
||||||
|
"""Strip BLE-MIDI header/timestamp bytes; keep App SysEx (F0…F7).
|
||||||
|
|
||||||
|
- Framed: [header ts][optional ts][F0 … ts … F7]
|
||||||
|
- Raw (some ATS2853 notifies): [F0 … F7] — must not treat F0 as header
|
||||||
|
(F0/F7 also have bit7=1).
|
||||||
|
"""
|
||||||
|
if not payload:
|
||||||
|
return b""
|
||||||
|
i = 0
|
||||||
|
if payload[0] not in (0xF0, 0xF7) and (payload[0] & 0x80):
|
||||||
|
i = 1 # BLE-MIDI header
|
||||||
|
out = bytearray()
|
||||||
|
while i < len(payload):
|
||||||
|
b = payload[i]
|
||||||
|
i += 1
|
||||||
|
if b in (0xF0, 0xF7):
|
||||||
|
out.append(b)
|
||||||
|
elif b & 0x80:
|
||||||
|
continue # timestamp inside/around SysEx
|
||||||
|
else:
|
||||||
|
out.append(b)
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
class BleMidiSim:
|
||||||
|
"""与 AppSim 同接口: send / read_frame / drain / query / close"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name=DEFAULT_NAME,
|
||||||
|
address=None,
|
||||||
|
scan_timeout=20.0,
|
||||||
|
transport="midi",
|
||||||
|
unpair=False,
|
||||||
|
):
|
||||||
|
from bleak import BleakClient, BleakScanner
|
||||||
|
|
||||||
|
self._BleakClient = BleakClient
|
||||||
|
self._BleakScanner = BleakScanner
|
||||||
|
self.parser = FrameParser()
|
||||||
|
self.pending = []
|
||||||
|
self._rx = queue.Queue()
|
||||||
|
self._client = None
|
||||||
|
self._write_char = None
|
||||||
|
self._notify_chars = []
|
||||||
|
self._transport = transport
|
||||||
|
self._unpair = unpair
|
||||||
|
self._loop = asyncio.new_event_loop()
|
||||||
|
self._thread = threading.Thread(target=self._loop.run_forever, daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
self.info = self._run(self._connect(name, address, scan_timeout))
|
||||||
|
|
||||||
|
def _run(self, coro, timeout=90.0):
|
||||||
|
return asyncio.run_coroutine_threadsafe(coro, self._loop).result(timeout)
|
||||||
|
|
||||||
|
async def _find(self, name, address, scan_timeout):
|
||||||
|
if address:
|
||||||
|
return await self._BleakScanner.find_device_by_address(
|
||||||
|
address, timeout=scan_timeout
|
||||||
|
)
|
||||||
|
print(f"扫描 BLE 设备 ({scan_timeout:.0f}s) ...")
|
||||||
|
# name filter is more reliable than service UUID filter on Windows
|
||||||
|
t0 = time.monotonic()
|
||||||
|
while time.monotonic() - t0 < scan_timeout:
|
||||||
|
rem = max(1.0, scan_timeout - (time.monotonic() - t0))
|
||||||
|
d = await self._BleakScanner.find_device_by_filter(
|
||||||
|
lambda d, a: d.name and name.lower() in d.name.lower(),
|
||||||
|
timeout=min(8.0, rem),
|
||||||
|
)
|
||||||
|
if d:
|
||||||
|
print(f" 发现: {d.name!r} {d.address}")
|
||||||
|
return d
|
||||||
|
print(" ...")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _connect(self, name, address, scan_timeout):
|
||||||
|
dev = await self._find(name, address, scan_timeout)
|
||||||
|
if dev is None:
|
||||||
|
raise RuntimeError(f"未找到设备 {name!r}")
|
||||||
|
|
||||||
|
if self._unpair:
|
||||||
|
try:
|
||||||
|
tmp = self._BleakClient(dev.address, timeout=20)
|
||||||
|
await tmp.connect()
|
||||||
|
try:
|
||||||
|
await tmp.unpair()
|
||||||
|
print("已 unpair Windows 残留配对")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"unpair: {e}")
|
||||||
|
try:
|
||||||
|
await tmp.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
dev = await self._find(name, address, scan_timeout) or dev
|
||||||
|
except Exception as e:
|
||||||
|
print(f"unpair session: {e}")
|
||||||
|
|
||||||
|
client = self._BleakClient(dev, timeout=30)
|
||||||
|
await client.connect()
|
||||||
|
if not client.is_connected:
|
||||||
|
raise RuntimeError("BLE connect 后立即断开")
|
||||||
|
|
||||||
|
write_char = MIDI_CHAR if self._transport == "midi" else UART_WRITE
|
||||||
|
notify_list = (
|
||||||
|
[MIDI_CHAR]
|
||||||
|
if self._transport == "midi"
|
||||||
|
else [UART_NOTIFY, MIDI_CHAR]
|
||||||
|
)
|
||||||
|
|
||||||
|
max_payload = DEFAULT_MTU_PAYLOAD
|
||||||
|
for svc in client.services:
|
||||||
|
for c in svc.characteristics:
|
||||||
|
if c.uuid.lower() == write_char.lower():
|
||||||
|
try:
|
||||||
|
max_payload = c.max_write_without_response_size or max_payload
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_notify(_sender, data):
|
||||||
|
self._rx.put(bytes(data))
|
||||||
|
|
||||||
|
for u in notify_list:
|
||||||
|
try:
|
||||||
|
await client.start_notify(u, _on_notify)
|
||||||
|
self._notify_chars.append(u)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"notify fail {u[:8]}: {e}")
|
||||||
|
|
||||||
|
if not self._notify_chars:
|
||||||
|
raise RuntimeError("无法开启任何 notify(常见原因: Windows 残留配对)")
|
||||||
|
|
||||||
|
self._client = client
|
||||||
|
self._write_char = write_char
|
||||||
|
self._max_payload = max(DEFAULT_MTU_PAYLOAD, max_payload or DEFAULT_MTU_PAYLOAD)
|
||||||
|
return {
|
||||||
|
"name": getattr(dev, "name", None),
|
||||||
|
"address": getattr(dev, "address", address),
|
||||||
|
"max_payload": self._max_payload,
|
||||||
|
"transport": self._transport,
|
||||||
|
"notify": list(self._notify_chars),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _disconnect(self):
|
||||||
|
if self._client is None:
|
||||||
|
return
|
||||||
|
for u in self._notify_chars:
|
||||||
|
try:
|
||||||
|
await self._client.stop_notify(u)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
await self._client.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
try:
|
||||||
|
self._run(self._disconnect(), timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._loop.call_soon_threadsafe(self._loop.stop)
|
||||||
|
self._thread.join(timeout=5)
|
||||||
|
|
||||||
|
def _pump(self):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data = self._rx.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
if self._transport == "midi":
|
||||||
|
midi = ble_midi_decode_packet(data)
|
||||||
|
else:
|
||||||
|
# uart notify may be raw SysEx, or occasionally BLE-MIDI wrapped
|
||||||
|
midi = (
|
||||||
|
ble_midi_decode_packet(data)
|
||||||
|
if data and (data[0] & 0x80)
|
||||||
|
else data
|
||||||
|
)
|
||||||
|
self.pending.extend(self.parser.feed(midi))
|
||||||
|
|
||||||
|
def send(self, frame: bytes):
|
||||||
|
if self._transport == "midi":
|
||||||
|
pkts = ble_midi_encode_sysex(frame, self._max_payload)
|
||||||
|
else:
|
||||||
|
pkts = [frame]
|
||||||
|
for pkt in pkts:
|
||||||
|
self._run(
|
||||||
|
self._client.write_gatt_char(self._write_char, pkt, response=False),
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_frame(self, timeout=1.0, want_dev=False):
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
while True:
|
||||||
|
self._pump()
|
||||||
|
for i, f in enumerate(self.pending):
|
||||||
|
is_dev = len(f) > 1 and f[1] == DEV_ID
|
||||||
|
if is_dev == want_dev:
|
||||||
|
return self.pending.pop(i)
|
||||||
|
self.pending.clear()
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
return None
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
def drain(self, quiet=0.2):
|
||||||
|
while self.read_frame(timeout=quiet) is not None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def query(self, frame: bytes, timeout=3.0, retry=True):
|
||||||
|
self.drain(quiet=0.15)
|
||||||
|
self.send(frame)
|
||||||
|
r = self.read_frame(timeout=timeout)
|
||||||
|
if r is None and retry:
|
||||||
|
# BLE 偶发丢 Notify:短间隔重发一次(关机指令不可重试)
|
||||||
|
time.sleep(0.25)
|
||||||
|
self.send(frame)
|
||||||
|
r = self.read_frame(timeout=timeout)
|
||||||
|
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):
|
||||||
|
"""Drop session, unpair, rescan, reconnect — recovers Windows stale pairing."""
|
||||||
|
await self._disconnect()
|
||||||
|
self._notify_chars = []
|
||||||
|
self._client = None
|
||||||
|
await asyncio.sleep(0.8)
|
||||||
|
self._unpair = True
|
||||||
|
return await self._connect(name, address, scan_timeout)
|
||||||
|
|
||||||
|
def probe_or_rebind(self, name=DEFAULT_NAME, address=None, scan_timeout=20.0):
|
||||||
|
"""Send 01 01; on timeout, unpair+reconnect once and retry."""
|
||||||
|
r = self.query(build(0x01, 0x01), timeout=3.0)
|
||||||
|
if r is not None and len(r) >= 5 and r[0] == HEAD and r[1] == 0x60:
|
||||||
|
return True
|
||||||
|
print("首包无应答 → 自动 unpair 并重连 …")
|
||||||
|
try:
|
||||||
|
self.info = self._run(
|
||||||
|
self._reconnect_clean(name, address, scan_timeout), timeout=120.0
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"重连失败: {e}")
|
||||||
|
return False
|
||||||
|
r = self.query(build(0x01, 0x01), timeout=3.0)
|
||||||
|
ok = r is not None and len(r) >= 5 and r[0] == HEAD and r[1] == 0x60
|
||||||
|
if not ok:
|
||||||
|
print(
|
||||||
|
"仍无应答。请 JLink/断电复位吉他(E49A 裸写或异常断连后模组常停广播),"
|
||||||
|
"并确认琴已开机且蓝牙开关为开。"
|
||||||
|
)
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
def write_report(R: Results, info: dict, path: str, notes: list[str] | None = None):
|
||||||
|
n = {
|
||||||
|
s: sum(1 for r in R.rows if r[1] == s)
|
||||||
|
for s in ("PASS", "FAIL", "SKIP", "SENT")
|
||||||
|
}
|
||||||
|
lines = [
|
||||||
|
"# BLE SysEx 协议测试报告",
|
||||||
|
"",
|
||||||
|
f"- 日期: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}",
|
||||||
|
f"- 设备: {info.get('name')} ({info.get('address')})",
|
||||||
|
f"- 传输: {info.get('transport')} / bleak, payload={info.get('max_payload')}",
|
||||||
|
f"- notify: {info.get('notify')}",
|
||||||
|
"- 协议来源: Doc/指令测试.docx",
|
||||||
|
"- 测试脚本: tools/test_protocol_ble.py",
|
||||||
|
"",
|
||||||
|
"## 汇总",
|
||||||
|
"",
|
||||||
|
"| PASS | FAIL | SKIP | SENT |",
|
||||||
|
"|------|------|------|------|",
|
||||||
|
f"| {n['PASS']} | {n['FAIL']} | {n['SKIP']} | {n['SENT']} |",
|
||||||
|
"",
|
||||||
|
"## 明细",
|
||||||
|
"",
|
||||||
|
"| # | 用例 | 结果 | 详情 |",
|
||||||
|
"|---|------|------|------|",
|
||||||
|
]
|
||||||
|
for i, (name, status, detail) in enumerate(R.rows, 1):
|
||||||
|
lines.append(f"| {i} | {name} | {status} | {detail} |")
|
||||||
|
lines += ["", "## 备注", ""]
|
||||||
|
for note in notes or []:
|
||||||
|
lines.append(f"- {note}")
|
||||||
|
lines += [
|
||||||
|
"- `01 03`=`Version[]`;`01 04/05`=日期;`01 0C`=`branch_short6`(如 develop_0aedb4);`01 0F`=`BRS08L`。",
|
||||||
|
"- `01 07` UID 为 24 字符 hex ASCII(96-bit),保证 SysEx 7-bit 安全。",
|
||||||
|
"- `04 xx`/`06 xx` 无应答,SENT 表示已发送。",
|
||||||
|
"- `FD 01` 升级指令不在本协议范围。",
|
||||||
|
]
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(lines) + "\n")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def smoke(sim: BleMidiSim) -> Results:
|
||||||
|
R = Results()
|
||||||
|
r = sim.query(build(0x01, 0x01), timeout=3.0)
|
||||||
|
ok = r is not None and len(r) >= 5 and r[0] == HEAD and r[1] == 0x60
|
||||||
|
R.add("smoke 01 01 连接设备", "PASS" if ok else "FAIL", hexs(r))
|
||||||
|
if ok:
|
||||||
|
r2 = sim.query(build(0x01, 0x02), timeout=3.0)
|
||||||
|
ok2 = r2 is not None and r2[2:4] == bytes([0x01, 0x02])
|
||||||
|
R.add("smoke 01 02 设备名", "PASS" if ok2 else "FAIL", hexs(r2))
|
||||||
|
r3 = sim.query(build(0x02, 0x01), timeout=3.0)
|
||||||
|
ok3 = r3 is not None and len(r3) == 47
|
||||||
|
R.add("smoke 02 01 和弦表(长帧)", "PASS" if ok3 else "FAIL", hexs(r3))
|
||||||
|
return R
|
||||||
|
|
||||||
|
|
||||||
|
def selftest():
|
||||||
|
R = Results()
|
||||||
|
pkts = ble_midi_encode_sysex(bytes.fromhex("F0600101F7"), 20)
|
||||||
|
ok = pkts == [bytes.fromhex("8080F0600101F7")]
|
||||||
|
R.add("encode short", "PASS" if ok else "FAIL", str([hexs(p) for p in pkts]))
|
||||||
|
frame = bytes([0xF0, 0x60, 0x02, 0x01]) + bytes(range(1, 43)) + bytes([0xF7])
|
||||||
|
pkts = ble_midi_encode_sysex(frame, 20)
|
||||||
|
back = b"".join(ble_midi_decode_packet(p) for p in pkts)
|
||||||
|
R.add(
|
||||||
|
"encode/decode 47B",
|
||||||
|
"PASS" if back == frame and len(pkts) >= 3 else "FAIL",
|
||||||
|
f"{len(pkts)} pkts",
|
||||||
|
)
|
||||||
|
return R.summary()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="BLE 吉他协议全量测试")
|
||||||
|
ap.add_argument("--ble-name", default=DEFAULT_NAME)
|
||||||
|
ap.add_argument("--ble-address")
|
||||||
|
ap.add_argument(
|
||||||
|
"--transport",
|
||||||
|
choices=("midi", "uart"),
|
||||||
|
default="midi",
|
||||||
|
help="midi=标准 BLE-MIDI 帧; uart=自定义 e49a 原始 SysEx(裸写易弄挂模组)",
|
||||||
|
)
|
||||||
|
ap.add_argument(
|
||||||
|
"--unpair",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
default=(sys.platform == "win32"),
|
||||||
|
help="连接前 unpair(Windows 默认开;--no-unpair 可关)",
|
||||||
|
)
|
||||||
|
ap.add_argument("--allow-poweroff", action="store_true")
|
||||||
|
ap.add_argument("--smoke", action="store_true", help="仅冒烟: 01 01/02 + 02 01")
|
||||||
|
ap.add_argument("--selftest", action="store_true")
|
||||||
|
ap.add_argument("--report")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if args.selftest:
|
||||||
|
sys.exit(selftest())
|
||||||
|
try:
|
||||||
|
import bleak # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
sys.exit("缺少 bleak: pip install bleak")
|
||||||
|
|
||||||
|
print(f"选项: unpair={args.unpair} transport={args.transport}")
|
||||||
|
try:
|
||||||
|
sim = BleMidiSim(
|
||||||
|
name=args.ble_name,
|
||||||
|
address=args.ble_address,
|
||||||
|
transport=args.transport,
|
||||||
|
unpair=args.unpair,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
R = Results()
|
||||||
|
R.add("BLE 扫描连接", "FAIL", str(exc))
|
||||||
|
R.summary()
|
||||||
|
print(f"[FAIL] BLE 扫描连接 | {exc}")
|
||||||
|
sys.exit(1)
|
||||||
|
print(
|
||||||
|
f"=== BLE 测试: {sim.info.get('name')} ({sim.info.get('address')}) "
|
||||||
|
f"transport={sim.info.get('transport')} payload={sim.info.get('max_payload')} ==="
|
||||||
|
)
|
||||||
|
if not sim.probe_or_rebind(args.ble_name, args.ble_address):
|
||||||
|
sim.close()
|
||||||
|
path = args.report or os.path.normpath(
|
||||||
|
os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"Doc",
|
||||||
|
"reports",
|
||||||
|
f"ble_sysex_{datetime.datetime.now():%Y%m%d_%H%M%S}.md",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
R = Results()
|
||||||
|
R.add("probe 01 01", "FAIL", "timeout after unpair/reconnect")
|
||||||
|
write_report(
|
||||||
|
R,
|
||||||
|
sim.info or {},
|
||||||
|
path,
|
||||||
|
notes=[
|
||||||
|
"首包无应答:多半是 Windows 残留配对,或 ATS2853 被 E49A 裸写弄挂需复位。",
|
||||||
|
"推荐: python tools/test_protocol_ble.py --unpair",
|
||||||
|
"仍失败则 JLink reset / 给吉他断电再开机后再测。",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
print(f"\n报告已写入: {path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
notes = [
|
||||||
|
f"transport={sim.info.get('transport')}",
|
||||||
|
f"unpair={args.unpair}",
|
||||||
|
"Windows 默认 --unpair;全 timeout 时先复位吉他再测。",
|
||||||
|
"勿用 --transport uart 裸写做冒烟(易停广播)。",
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
R = smoke(sim) if args.smoke else run_tests(sim, allow_poweroff=args.allow_poweroff)
|
||||||
|
finally:
|
||||||
|
sim.close()
|
||||||
|
code = R.summary()
|
||||||
|
|
||||||
|
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
path = args.report or os.path.normpath(
|
||||||
|
os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"Doc",
|
||||||
|
"reports",
|
||||||
|
f"ble_sysex_{ts}.md",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
write_report(R, sim.info, path, notes=notes)
|
||||||
|
print(f"\n报告已写入: {path}")
|
||||||
|
sys.exit(code)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""音师自助:把投放目录的 1/2/3.bin 打成 extflash_ALL_*.res,供 SoundWalkerIAP USB 烧录。
|
||||||
|
|
||||||
|
不走 J-Link;不改固件头文件;不重编 MCU。
|
||||||
|
需要本机已有 ui0902_res.bin(或最近一次发布包里的 ALL.res 可拆出 UI 段)。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1] # firmware project
|
||||||
|
REPO = ROOT.parent.parent # 一诺国际吉他
|
||||||
|
OUT_DIR = ROOT / "tools" / "out"
|
||||||
|
DROP_DIR = REPO / "Doc" / "音色文件" / "音师投放"
|
||||||
|
IAP_DIR = REPO / "升级" / "MCU主控升级"
|
||||||
|
UI0902_BIN = OUT_DIR / "ui0902_res.bin"
|
||||||
|
REPO_OUT = REPO / "tools" / "out"
|
||||||
|
|
||||||
|
OFF_LOGO = 0x00000000
|
||||||
|
OFF_CHARGING = 0x0000CB70
|
||||||
|
OFF_BIN1 = 0x0001B8F0
|
||||||
|
OFF_BIN2 = 0x0009D07D
|
||||||
|
OFF_BIN3 = 0x000A71AC
|
||||||
|
UI0902_RES_BASE = 0x00100000
|
||||||
|
DAB_MAGIC = b"\xABDAB"
|
||||||
|
MAX_SONG_BIN_BYTES = 41 * 1024
|
||||||
|
BIN2_SLOT_BYTES = OFF_BIN3 - OFF_BIN2
|
||||||
|
|
||||||
|
|
||||||
|
def find_ziliao() -> Path:
|
||||||
|
for p in REPO.iterdir():
|
||||||
|
if p.is_dir() and (p / "logo.bin").is_file() and (p / "Charg.bin").is_file():
|
||||||
|
return p
|
||||||
|
raise SystemExit("找不到 资料/logo.bin 与 Charg.bin,无法打包。")
|
||||||
|
|
||||||
|
|
||||||
|
def dab_ok(blob: bytes, off: int, expect_cnt: int, label: str) -> None:
|
||||||
|
if off + 16 > len(blob):
|
||||||
|
raise SystemExit(f"{label} @0x{off:X}: 文件太短,无法校验")
|
||||||
|
magic = blob[off : off + 4]
|
||||||
|
cnt = struct.unpack_from("<I", blob, off + 12)[0]
|
||||||
|
if magic != DAB_MAGIC:
|
||||||
|
raise SystemExit(
|
||||||
|
f"{label} @0x{off:X}: DAB 魔数错误 {magic.hex()}(期望 {DAB_MAGIC.hex()}),请确认文件是否正确导出"
|
||||||
|
)
|
||||||
|
if cnt != expect_cnt:
|
||||||
|
raise SystemExit(
|
||||||
|
f"{label} @0x{off:X}: 预设数量={cnt},期望 {expect_cnt}。"
|
||||||
|
f"{'若曲目数量有变,需找开发同步固件曲名表。' if label.startswith('2.bin') else ''}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def require_equals(blob: bytes, off: int, src: bytes, label: str) -> None:
|
||||||
|
end = off + len(src)
|
||||||
|
if end > len(blob) or blob[off:end] != src:
|
||||||
|
raise SystemExit(f"内部校验失败:{label} 未正确写入资源包")
|
||||||
|
|
||||||
|
|
||||||
|
def find_ui_baseline() -> bytes:
|
||||||
|
if UI0902_BIN.is_file():
|
||||||
|
data = UI0902_BIN.read_bytes()
|
||||||
|
print(f"UI 底图: {UI0902_BIN} ({len(data)} bytes)")
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Fallback: newest release ALL.res under repo tools/out
|
||||||
|
candidates: list[Path] = []
|
||||||
|
if REPO_OUT.is_dir():
|
||||||
|
candidates.extend(REPO_OUT.glob("**/extflash_ALL_*.res"))
|
||||||
|
candidates.extend(REPO_OUT.glob("extflash_ALL_*.res"))
|
||||||
|
candidates = sorted({p.resolve() for p in candidates if p.is_file()}, key=lambda p: p.stat().st_mtime, reverse=True)
|
||||||
|
for all_path in candidates:
|
||||||
|
raw = all_path.read_bytes()
|
||||||
|
if len(raw) > UI0902_RES_BASE:
|
||||||
|
ui = raw[UI0902_RES_BASE:]
|
||||||
|
print(f"UI 底图: 从发布包拆出 {all_path.name} @0x{UI0902_RES_BASE:X} ({len(ui)} bytes)")
|
||||||
|
return ui
|
||||||
|
|
||||||
|
raise SystemExit(
|
||||||
|
"缺少 UI 底图 ui0902_res.bin。\n"
|
||||||
|
"请向开发索取一次含 UI 的基线(tools/out/ui0902_res.bin,或完整 extflash_ALL_*.res),"
|
||||||
|
"放到工程 tools/out/ 后再运行本工具。"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pack_tone(bin1: bytes, bin2: bytes, bin3: bytes, logo: bytes, charg: bytes) -> bytes:
|
||||||
|
if len(bin2) > MAX_SONG_BIN_BYTES:
|
||||||
|
raise SystemExit(f"2.bin 过大: {len(bin2)} > {MAX_SONG_BIN_BYTES} (41KB 上限)")
|
||||||
|
if len(bin2) > BIN2_SLOT_BYTES:
|
||||||
|
raise SystemExit(
|
||||||
|
f"2.bin 过大: {len(bin2)} > 槽位 {BIN2_SLOT_BYTES},会挤占 3.bin 固定地址 0x{OFF_BIN3:X}"
|
||||||
|
)
|
||||||
|
|
||||||
|
parts = [
|
||||||
|
(OFF_LOGO, logo, "logo"),
|
||||||
|
(OFF_CHARGING, charg, "Charg"),
|
||||||
|
(OFF_BIN1, bin1, "1.bin"),
|
||||||
|
(OFF_BIN2, bin2, "2.bin"),
|
||||||
|
(OFF_BIN3, bin3, "3.bin"),
|
||||||
|
]
|
||||||
|
blobs: list[bytes] = []
|
||||||
|
cursor = 0
|
||||||
|
for force_off, data, name in parts:
|
||||||
|
if cursor > force_off:
|
||||||
|
raise SystemExit(f"{name}: 前一段过大,无法放到 0x{force_off:X}")
|
||||||
|
if cursor < force_off:
|
||||||
|
blobs.append(b"\xFF" * (force_off - cursor))
|
||||||
|
cursor = force_off
|
||||||
|
blobs.append(data)
|
||||||
|
cursor += len(data)
|
||||||
|
|
||||||
|
packed = b"".join(blobs)
|
||||||
|
dab_ok(packed, OFF_BIN1, 31, "1.bin(节奏)")
|
||||||
|
dab_ok(packed, OFF_BIN2, 1, "2.bin(本地曲目)")
|
||||||
|
dab_ok(packed, OFF_BIN3, 3, "3.bin(万能)")
|
||||||
|
require_equals(packed, OFF_BIN1, bin1, "1.bin")
|
||||||
|
require_equals(packed, OFF_BIN2, bin2, "2.bin")
|
||||||
|
require_equals(packed, OFF_BIN3, bin3, "3.bin")
|
||||||
|
if cursor > UI0902_RES_BASE:
|
||||||
|
raise SystemExit("音色区溢出到 UI 区 (0x100000)")
|
||||||
|
return packed
|
||||||
|
|
||||||
|
|
||||||
|
def write_flash_guide(path: Path, all_name: str, stamp: str) -> None:
|
||||||
|
text = "\n".join(
|
||||||
|
[
|
||||||
|
"音师音色自助包 — 请这样用 SoundWalkerIAP 刷机",
|
||||||
|
f"生成时间: {stamp}",
|
||||||
|
"",
|
||||||
|
"==== 本次只需刷外部 Flash(不用重刷 Boot/APP,除非开发另有说明) ====",
|
||||||
|
f" 文件: {all_name}",
|
||||||
|
" 地址: 0x00000000(从外部 Flash 开头整包写入)",
|
||||||
|
"",
|
||||||
|
"步骤:",
|
||||||
|
" 1. 吉他进入 USB 升级模式(参见 升级步骤.docx)",
|
||||||
|
" 2. 打开本目录的 SoundWalkerIAP.exe",
|
||||||
|
" 3. 先整片擦除外部 Flash",
|
||||||
|
f" 4. 选择 {all_name},烧录到外部 Flash @ 0x00000000",
|
||||||
|
" 5. 退出升级模式,重启,试听节奏/本地曲/万能",
|
||||||
|
"",
|
||||||
|
"注意:",
|
||||||
|
" - 必须刷本包 ALL.res,不要只刷单独的 1/2/3.bin",
|
||||||
|
" - 只刷音色、不带 UI 会导致模式选择页花屏",
|
||||||
|
" - 2.bin 必须 ≤41KB;3.bin 地址固定,不可随 2.bin 变长漂移",
|
||||||
|
" - 若增删本地曲目或改曲名,需找开发同步固件后再测",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
path.write_text(text, encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser(description="音师自助打包 1/2/3.bin → ALL.res(USB IAP)")
|
||||||
|
ap.add_argument(
|
||||||
|
"--drop",
|
||||||
|
type=Path,
|
||||||
|
default=DROP_DIR,
|
||||||
|
help=f"投放目录(默认 {DROP_DIR})",
|
||||||
|
)
|
||||||
|
ap.add_argument(
|
||||||
|
"--open",
|
||||||
|
action="store_true",
|
||||||
|
help="完成后用资源管理器打开输出目录",
|
||||||
|
)
|
||||||
|
args = ap.parse_args()
|
||||||
|
drop: Path = args.drop
|
||||||
|
|
||||||
|
print("=== 音师音色打包(USB / SoundWalkerIAP,非 J-Link)===")
|
||||||
|
print(f"投放目录: {drop}")
|
||||||
|
|
||||||
|
bin1_p = drop / "1.bin"
|
||||||
|
bin2_p = drop / "2.bin"
|
||||||
|
bin3_p = drop / "3.bin"
|
||||||
|
missing = [p.name for p in (bin1_p, bin2_p, bin3_p) if not p.is_file()]
|
||||||
|
if missing:
|
||||||
|
raise SystemExit(
|
||||||
|
f"投放目录缺少: {', '.join(missing)}\n"
|
||||||
|
f"请把新的 1.bin / 2.bin / 3.bin 放到:\n {drop}"
|
||||||
|
)
|
||||||
|
|
||||||
|
ziliao = find_ziliao()
|
||||||
|
bin1 = bin1_p.read_bytes()
|
||||||
|
bin2 = bin2_p.read_bytes()
|
||||||
|
bin3 = bin3_p.read_bytes()
|
||||||
|
logo = (ziliao / "logo.bin").read_bytes()
|
||||||
|
charg = (ziliao / "Charg.bin").read_bytes()
|
||||||
|
print(f"1.bin {len(bin1)} bytes | 2.bin {len(bin2)} bytes | 3.bin {len(bin3)} bytes")
|
||||||
|
|
||||||
|
tone = pack_tone(bin1, bin2, bin3, logo, charg)
|
||||||
|
ui = find_ui_baseline()
|
||||||
|
all_res = tone + (b"\xFF" * (UI0902_RES_BASE - len(tone))) + ui
|
||||||
|
require_equals(all_res, OFF_BIN1, bin1, "ALL/1.bin")
|
||||||
|
require_equals(all_res, OFF_BIN2, bin2, "ALL/2.bin")
|
||||||
|
require_equals(all_res, OFF_BIN3, bin3, "ALL/3.bin")
|
||||||
|
dab_ok(all_res, OFF_BIN1, 31, "ALL/1.bin")
|
||||||
|
dab_ok(all_res, OFF_BIN2, 1, "ALL/2.bin")
|
||||||
|
dab_ok(all_res, OFF_BIN3, 3, "ALL/3.bin")
|
||||||
|
|
||||||
|
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
day = datetime.now().strftime("%Y%m%d")
|
||||||
|
pkg = OUT_DIR / f"音师音色包_{day}"
|
||||||
|
pkg.mkdir(parents=True, exist_ok=True)
|
||||||
|
all_name = f"extflash_ALL_artist_{stamp}.res"
|
||||||
|
all_path = pkg / all_name
|
||||||
|
all_path.write_bytes(all_res)
|
||||||
|
(OUT_DIR / "extflash_tone_artist.res").write_bytes(tone)
|
||||||
|
(OUT_DIR / "extflash_ALL_artist.res").write_bytes(all_res)
|
||||||
|
|
||||||
|
iap_exe = IAP_DIR / "SoundWalkerIAP.exe"
|
||||||
|
iap_doc = IAP_DIR / "升级步骤.docx"
|
||||||
|
if not iap_exe.is_file():
|
||||||
|
raise SystemExit(f"找不到 {iap_exe}")
|
||||||
|
shutil.copy2(iap_exe, pkg / "SoundWalkerIAP.exe")
|
||||||
|
if iap_doc.is_file():
|
||||||
|
shutil.copy2(iap_doc, pkg / "升级步骤.docx")
|
||||||
|
write_flash_guide(pkg / "请这样刷.txt", all_name, stamp)
|
||||||
|
|
||||||
|
print(f"OK 音色区 {len(tone)} bytes,ALL.res {len(all_res)} bytes")
|
||||||
|
print(f"OK 输出目录: {pkg}")
|
||||||
|
print(f" - {all_name}")
|
||||||
|
print(" - SoundWalkerIAP.exe")
|
||||||
|
print(" - 请这样刷.txt")
|
||||||
|
print("请用 USB + SoundWalkerIAP 刷 ALL.res,不要用 J-Link 单独烧 bin。")
|
||||||
|
|
||||||
|
if args.open:
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
|
||||||
|
os.startfile(str(pkg)) # type: ignore[attr-defined]
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"(无法自动打开目录: {exc})")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
sys.exit(main())
|
||||||
|
except SystemExit as e:
|
||||||
|
code = e.code
|
||||||
|
if isinstance(code, str):
|
||||||
|
print(f"\n失败: {code}")
|
||||||
|
sys.exit(1)
|
||||||
|
if code not in (0, None):
|
||||||
|
sys.exit(int(code) if isinstance(code, int) else 1)
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n失败: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
音师投放与自助更新说明
|
||||||
|
========================
|
||||||
|
|
||||||
|
一、投放目录(放新的 1/2/3.bin)
|
||||||
|
一诺国际吉他\Doc\音色文件\音师投放\
|
||||||
|
1.bin 普通/专业节奏
|
||||||
|
2.bin 本地曲目(必须 ≤41KB)
|
||||||
|
3.bin 万能模式
|
||||||
|
|
||||||
|
二、一键打包(合并 ALL.res,USB 烧录,不用 J-Link)
|
||||||
|
双击:本目录「音师更新音色.bat」
|
||||||
|
或命令行:python tone_artist_pack.py --open
|
||||||
|
|
||||||
|
三、烧录
|
||||||
|
输出在 tools\out\音师音色包_日期\
|
||||||
|
- extflash_ALL_artist_*.res
|
||||||
|
- SoundWalkerIAP.exe
|
||||||
|
- 请这样刷.txt
|
||||||
|
按「请这样刷.txt」:进 USB 升级模式 → 擦除外部 Flash
|
||||||
|
→ SoundWalkerIAP 将 ALL.res 烧到 0x00000000 → 重启试听
|
||||||
|
|
||||||
|
四、限制
|
||||||
|
- 只更新音色时一般不用重刷 MCU
|
||||||
|
- 必须刷 ALL.res(含界面),勿只刷单独 bin,否则模式选择花屏
|
||||||
|
- 增删本地曲/改曲名需找开发同步固件曲名表
|
||||||
|
- 首次需本机有 tools\out\ui0902_res.bin(向开发索取一次基线即可)
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
setlocal
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo 音师音色更新 - 打包 ALL.res (USB IAP)
|
||||||
|
echo 不使用 J-Link,请用 SoundWalkerIAP 烧录
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
where python >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [错误] 未找到 python,请先安装 Python 3 并勾选 Add to PATH。
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
python "%~dp0tone_artist_pack.py" --open
|
||||||
|
set ERR=%ERRORLEVEL%
|
||||||
|
echo.
|
||||||
|
if not "%ERR%"=="0" (
|
||||||
|
echo 打包失败,请根据上方中文提示检查 1/2/3.bin。
|
||||||
|
pause
|
||||||
|
exit /b %ERR%
|
||||||
|
)
|
||||||
|
|
||||||
|
echo 打包成功。请按输出目录中的「请这样刷.txt」用 SoundWalkerIAP 刷机。
|
||||||
|
pause
|
||||||
|
exit /b 0
|
||||||
Loading…
Reference in New Issue