Compare commits
50 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
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
|
||||
*.pem
|
||||
credentials.json
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
|
|
|||
|
|
@ -303,6 +303,14 @@ void app_adc_sync_volume(uint16_t 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)
|
||||
{
|
||||
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);
|
||||
/* 开机/主动采样后同步 vol_last 与 level,避免扫描任务误触发或跳变 */
|
||||
void app_adc_sync_volume(uint16_t val);
|
||||
/* 取当前主音量(0~127),供 MasterVolume_Reapply 等重发场景使用 */
|
||||
uint8_t app_adc_get_volume(void);
|
||||
#endif
|
||||
254
APP/app_log.c
|
|
@ -11,7 +11,7 @@ static uint32_t s_overflow;
|
|||
static uint32_t s_boot_count;
|
||||
static char s_ui_page[16] = "boot";
|
||||
static char s_debug_level = 'D';
|
||||
static char s_cmd_buf[48];
|
||||
static char s_cmd_buf[64];
|
||||
static uint8_t s_cmd_len;
|
||||
|
||||
/* 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, ...)
|
||||
{
|
||||
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;
|
||||
int n;
|
||||
|
||||
/* TMR6 1ms ISR 会经 AutoBand NoteOn 回调进来:禁止在中断里 take mutex / 打 RTT */
|
||||
if (rt_interrupt_get_nest() != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (level == 'D' && s_debug_level != 'D') {
|
||||
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] ",
|
||||
(unsigned)app_log_ms(), level, cat);
|
||||
if (n < 0) {
|
||||
if (s_log_mtx != RT_NULL) {
|
||||
rt_mutex_release(s_log_mtx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
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);
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||
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)
|
||||
|
|
@ -274,12 +297,24 @@ uint8_t app_log_try_command(const char *cmd)
|
|||
app_log_flash_start(size);
|
||||
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) {
|
||||
uint32_t size = (uint32_t)strtoul(cmd + 14, NULL, 10);
|
||||
app_log_flash_start_at(FLASH_ADDR_SONG_HAITIAN, size,
|
||||
"FLASH_HAITIAN_GO", "FLASH_HAITIAN_OK\n");
|
||||
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) {
|
||||
uint8_t hdr[16];
|
||||
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) |
|
||||
(cur_hdr[14] << 16) | (cur_hdr[15] << 24)));
|
||||
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),
|
||||
"TONE @BIN2 magic=%02X%02X%02X%02X max=%lu cnt=%lu name=%.16s\n",
|
||||
hdr[0], hdr[1], hdr[2], hdr[3],
|
||||
|
|
@ -353,7 +398,7 @@ uint8_t app_log_try_command(const char *cmd)
|
|||
UI_ApplyToneAddress();
|
||||
AutoBandTop1_Stop();
|
||||
StartFlag = 0;
|
||||
ret = AutoBandTop1_LoadPresetItemFromFlash(0);
|
||||
ret = App_Auto_LoadPresetFromFlash(0);
|
||||
snprintf(line, sizeof(line),
|
||||
"TONE_LOCAL ret=%d addr=0x%08lX map=BIN2@0x%08lX name=%s count=%d %s\n",
|
||||
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);
|
||||
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) {
|
||||
/* 模拟专业+本地曲目拨片(无和弦板) */
|
||||
/* 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_AUTOBAND_SW].Current = 1;
|
||||
KEY_ID_1629 = 0; /* 走默认和弦兜底 */
|
||||
if (!(cmd[9] == ' ' && (cmd[10] == 'k' || cmd[10] == 'K'))) {
|
||||
KEY_ID_1629 = 0; /* 默认走一级和弦兜底 */
|
||||
}
|
||||
StartFlag = 0;
|
||||
Pick_Handle();
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_PICK_DONE\n");
|
||||
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) {
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "FLASH_ERASE_BEGIN\n");
|
||||
LOG_I("FLASH", "W25Q128 chip erase start");
|
||||
|
|
@ -383,24 +615,28 @@ uint8_t app_log_try_command(const char *cmd)
|
|||
return 1U;
|
||||
}
|
||||
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_WR_PIC_FROM_FLASH(0, 0, UI0902_BOOT_LOGO_W, UI0902_BOOT_LOGO_H,
|
||||
UI0902_BOOT_LOGO_ADDR);
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "UI_BOOT_OK\n");
|
||||
return 1U;
|
||||
}
|
||||
if (strncmp(cmd, "ui charge", 9) == 0) {
|
||||
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_FillByColor(0, 0, 240, 320, UI0902_BG_COLOR);
|
||||
W25Q128_Read(peek, UI0902_CHARGE_SCREEN_ADDR, 4);
|
||||
if (peek[0] != 0xFF || peek[1] != 0xFF) {
|
||||
LCD_WR_PIC_FROM_FLASH(0, 0, UI0902_CHARGE_SCREEN_W, UI0902_CHARGE_SCREEN_H,
|
||||
UI0902_CHARGE_SCREEN_ADDR);
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "UI_CHARGE_OK\n");
|
||||
} else {
|
||||
Draw_Charging_Icon(120, 150, 2, 0x3D7F);
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "UI_CHARGE_EMPTY\n");
|
||||
}
|
||||
{
|
||||
const char *s = "50";
|
||||
|
|
|
|||
|
|
@ -84,3 +84,13 @@ uint8_t app_tm1617_scan_key(void)
|
|||
}
|
||||
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);
|
||||
|
||||
/* 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 */
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
#include "includes.h"
|
||||
|
||||
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] = {
|
||||
// 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();
|
||||
rt_mutex_release(TM1629_Mutex);
|
||||
|
||||
if(key_hold_sticky && key == KEY_NONE)
|
||||
return KEY_NONE; /* ??????§ľ??????????????????????? */
|
||||
|
||||
if(key != key_last)
|
||||
{
|
||||
key_last = key;
|
||||
|
|
@ -38,6 +42,27 @@ uint8_t app_tm1629_Scan_Key(void)
|
|||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
void app_tm1629_init(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_led(LedNum_TypeDef led, LedColor_TypeDef color);
|
||||
void app_tm1629_all_off(void);
|
||||
|
|
|
|||
311
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 },
|
||||
{ 183, 229, UI0902_SECTION_Y, UI0902_SECTION_Y + UI0902_SECTION_BTN_H - 1, 0, 3, GUI_PLAY_SECTION, NULL },
|
||||
|
||||
/* 底栏四键:万能 | 普通 | 专业 | 设置(触区中线对齐 cx 40/100/160/210) */
|
||||
{ 0, 69, 280, 319, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
|
||||
{ 70, 129, 280, 319, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
|
||||
{ 130, 184, 280, 319, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
|
||||
{ 185, 239, 280, 319, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
|
||||
/* 底栏四键:万能 | 普通 | 专业 | 设置(60×55,y265–319) */
|
||||
{ UI0902_NAV_X0(0), UI0902_NAV_X1(0), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_UNIVERSAL, 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 },
|
||||
{ UI0902_NAV_X0(2), UI0902_NAV_X1(2), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_EXPERT, 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 上限附近 */
|
||||
|
|
@ -101,7 +101,7 @@ 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));
|
||||
|
||||
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,12}; /* major.minor.patch — 0.2.12: autoclose 关闭 glyph; normal pick piece; HIL/flash tools */
|
||||
|
||||
uint8_t Led = 8;
|
||||
bool PressFlag = 0;
|
||||
|
|
@ -161,6 +161,7 @@ void BLTask_Sendmsg(uint16_t ID, uint16_t ID2, uint16_t HiByte, uint16_t LoByte)
|
|||
|
||||
#define PWR_ON_HOLD_MS 3000 /* 关机态长按 3s → 开机 */
|
||||
#define PWR_OFF_HOLD_MS 2000 /* 开机态长按 2s → 关机 */
|
||||
#define PWR_OFF_GRACE_MS 1500 /* 请求优雅关机后,超时仍未切轨则紧急断电+复位 */
|
||||
|
||||
|
||||
typedef enum
|
||||
|
|
@ -176,14 +177,42 @@ static uint32_t pwr_press_tick = 0;
|
|||
/* 当前开关机状态(UI_Idle.c 等 extern 引用) */
|
||||
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)
|
||||
{
|
||||
LOG_I("PWR", "power_on begin");
|
||||
s_pwr_rails_cut = 0u;
|
||||
s_pwr_off_grace_ms = 0u;
|
||||
/* 控制电源硬件;蓝牙按 NVM/系统设置开关恢复,勿强制常开 */
|
||||
BSP_MainPowerEnable(1);
|
||||
BSP_DreamCorePowerEnable(1);
|
||||
BSP_HT7178PowerEnable(1);
|
||||
/* 蓝牙按 NVM/系统设置开关恢复,勿强制常开 */
|
||||
BSP_BlueToothPowerEnable((uint8_t)(mGuiData[GUI_BL_SW].Current ? 1 : 0));
|
||||
powon = true; /* 更新开机标志 */
|
||||
|
||||
|
|
@ -196,7 +225,9 @@ void System_PowerOn(void)
|
|||
app_tm1617_init();
|
||||
XPT2046_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)
|
||||
|
|
@ -210,6 +241,7 @@ static void PowerOff(void)
|
|||
|
||||
LOG_I("PWR", "power_off begin");
|
||||
powon = false; /* 更新关机标志 */
|
||||
s_pwr_off_grace_ms = 0u; /* 主路径已接管,取消紧急倒计时 */
|
||||
StopFullTask();
|
||||
|
||||
/* 关机前把当前调音台推子与蓝牙开关写入 NVM,保证再次开机保持 */
|
||||
|
|
@ -226,20 +258,19 @@ static void PowerOff(void)
|
|||
|
||||
LCD_FillByColor(0, 0, 240, 320, BLACK);
|
||||
LCD_BLK_Clr();
|
||||
LCD_WR_REG(0x28); /* Display OFF */
|
||||
|
||||
app_tm1629_all_off();
|
||||
|
||||
app_tm1617_off();
|
||||
/* 关闭电源硬件;MCU 保持运行以便长按开机 / 关机充电画面 */
|
||||
BSP_MainPowerEnable(0);
|
||||
BSP_DreamCorePowerEnable(0);
|
||||
BSP_HT7178PowerEnable(0);
|
||||
BSP_BlueToothPowerEnable(0);
|
||||
/* BT 先留窗口转发已排队 ACK,再切全部电源轨 */
|
||||
rt_thread_mdelay(30);
|
||||
PowerOff_CutRails();
|
||||
CurrUIProcress = IdleProcess;
|
||||
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;
|
||||
for (i = 0; i < 4; i++)
|
||||
|
|
@ -253,12 +284,109 @@ static void PowerOff(void)
|
|||
{
|
||||
usb_charging_state = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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):与主循环解耦的关机键扫描。
|
||||
* 正常:请求优雅关机;超时未切轨 → 紧急断电并复位。
|
||||
*/
|
||||
void Power_Key_IsrTick(void)
|
||||
{
|
||||
static uint16_t isr_hold_ms = 0;
|
||||
static uint8_t isr_fired = 0;
|
||||
flag_status st;
|
||||
|
||||
/* RTT/USB 烧 ExtFlash 期间禁止软关机,避免长按/粘键打断 FLASH_ACK */
|
||||
if (app_log_flash_busy())
|
||||
return;
|
||||
|
||||
st = BSP_GetPowerKey();
|
||||
|
||||
if (st == KEY_PRESS_LEVEL)
|
||||
{
|
||||
if (isr_hold_ms < 60000u)
|
||||
isr_hold_ms++;
|
||||
}
|
||||
else
|
||||
{
|
||||
isr_hold_ms = 0;
|
||||
isr_fired = 0;
|
||||
}
|
||||
|
||||
/* 仅处理「开机态关机」;开机仍由主循环负责(需完整外设初始化) */
|
||||
if (powon && !isr_fired && isr_hold_ms >= PWR_OFF_HOLD_MS)
|
||||
{
|
||||
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()
|
||||
{
|
||||
flag_status status = BSP_GetPowerKey();
|
||||
|
|
@ -317,19 +445,13 @@ void Power_Key_Scan()
|
|||
}
|
||||
holdtick = now - pwr_press_tick;
|
||||
|
||||
/* 用 powon 区分:关机长按 3s 开机,开机长按 2s 关机 */
|
||||
/* 关机长按 3s 开机;开机态关机改由 TMR7 ISR(主循环卡死仍可关) */
|
||||
if(powon == false && holdtick >= PWR_ON_HOLD_MS)
|
||||
{
|
||||
LOG_I("PWR", "key power_on hold=%u", (unsigned)holdtick);
|
||||
PowerOn();
|
||||
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;
|
||||
|
||||
case KEY_STATE_NONE:
|
||||
|
|
@ -437,6 +559,16 @@ void Send_volume(uint8_t vol)
|
|||
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)
|
||||
{
|
||||
uint8_t midi_note_buff[4] = {0,};
|
||||
|
|
@ -455,6 +587,29 @@ void Accomp_UpdatePlayingChord(void)
|
|||
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)
|
||||
{
|
||||
uint8_t midi_note_buff[4] = {0,};
|
||||
|
|
@ -464,13 +619,21 @@ void ADC_IN1_KEY_Handle(uint8_t key,bool on)
|
|||
if(key_toggle[key] == 1)
|
||||
{
|
||||
//ResetAutoPowerCount();
|
||||
if(key == 1)
|
||||
{
|
||||
/* 尾奏:与万能模式第4键共用同一起奏路径 */
|
||||
AutoBand_StartOutro();
|
||||
}
|
||||
else
|
||||
{
|
||||
switch(key)
|
||||
{
|
||||
case 3: AutoBandTop1_Preamble(0); is_preamble = 1; break;
|
||||
case 0: AutoBandTop1_Preamble(1); is_preamble = 1; break;
|
||||
case 1: AutoBandTop1_Postamble(0); break;
|
||||
case 2: AutoBandTop1_Postamble(1); break;
|
||||
}
|
||||
/* 前奏/断奏起奏前补发主音量,保证与图标一致 */
|
||||
MasterVolume_Reapply();
|
||||
//AutoBandTop1_Start();
|
||||
AutoBandTop1_SyncStart();
|
||||
wk_delay_ms(50);
|
||||
|
|
@ -486,6 +649,7 @@ void ADC_IN1_KEY_Handle(uint8_t key,bool on)
|
|||
AutoBandTop1_Note_On(midi_note_buff[i],0x50);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Stop_AutoBand();
|
||||
|
|
@ -615,13 +779,29 @@ void TM1629_Handle(uint8_t key)
|
|||
STRING_MIDI *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;
|
||||
|
||||
switch(key)
|
||||
{
|
||||
case 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;
|
||||
//tick_ever_positive = false;
|
||||
|
|
@ -640,6 +820,11 @@ void TM1629_Handle(uint8_t key)
|
|||
rt_timer_stop(OutTime_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 */
|
||||
KEY_ID_1629 = key;
|
||||
MIDI = (STRING_MIDI *)Chord_Midi_Table[group];
|
||||
|
|
@ -728,8 +913,19 @@ void Pick_Handle()
|
|||
uint8_t note_cnt = 0;
|
||||
uint8_t chord_id = KEY_ID_1629;
|
||||
|
||||
/* 左手未触发和弦板(或落在拍速/停止键)时:拨片默认发当前调一级大和弦伴奏 */
|
||||
if (chord_id == 0 || chord_id == 22 || chord_id == 23)
|
||||
/* 空库/加载失败时禁止起奏:否则 TMR6 ISR 内 AutoBand 可能 HardFault,
|
||||
* 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;
|
||||
chord_type_index_map[1].type = 0;
|
||||
|
|
@ -738,6 +934,10 @@ void Pick_Handle()
|
|||
note_cnt = GetChordNotesByType(chord_id, chord_type_index_map[chord_id].type, midi_note_buff);
|
||||
Return_Light_buff(KEY_ID_1629);
|
||||
|
||||
/* 首次起奏前补发主音量:防开机 SysEx 丢失后"图标低、实际大" */
|
||||
if (StartFlag == 0)
|
||||
MasterVolume_Reapply();
|
||||
|
||||
switch(mGuiData[GUI_TAB_INDEX].Current)
|
||||
{
|
||||
case 3:
|
||||
|
|
@ -760,36 +960,45 @@ void Pick_Handle()
|
|||
AutoBandTop1_SetLoopMode(0,1);
|
||||
break;
|
||||
case 0:
|
||||
if(PressFlag)
|
||||
{
|
||||
AutoBandTop1_SetLoopMode(0,1);
|
||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||
}else
|
||||
{
|
||||
/* 设置页内沿用万能:与主路径一致,始终 loop */
|
||||
AutoBandTop1_SetPiecePlayMode(1, 0, 480);
|
||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||
}
|
||||
AutoBandTop1_SetLoopMode(0, 1);
|
||||
StartFlag = 1;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
/* 普通:节奏/本地曲目均用 StartWithNote;切库后允许再次拨片起奏 */
|
||||
if (StartFlag == 0 || mGuiData[GUI_AUTOBAND_SW].Current)
|
||||
/* 普通:左手按住指板 + 拨片起奏;松手由 TM1629 停。
|
||||
* 节奏用 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)
|
||||
{
|
||||
AutoBandTop1_Stop();
|
||||
AutoBandTop1_SetLoopMode(0,1);
|
||||
AutoBandTop1_SetRunVar(mGuiData[GUI_PLAY_SECTION].Current);
|
||||
/* 起调必须用 0x3C+移调;勿用和弦音区 MIDI(如 48),否则本地曲目起奏异常 */
|
||||
midi = 0x3C + mGuiData[GUI_TRANSPOSE].Current;
|
||||
AutoBandTop1_StartWithNote(midi);
|
||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||
if (mGuiData[GUI_AUTOBAND_SW].Current)
|
||||
LOG_I("pick", "local/normal root=%u name=%s", midi,
|
||||
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;
|
||||
|
||||
case 1:
|
||||
|
|
@ -819,17 +1028,14 @@ void Pick_Handle()
|
|||
}
|
||||
break;
|
||||
case 0:
|
||||
/* 万能:和弦走向用 piece 模式;须 SetLoopMode,否则无循环时常听不到伴奏 */
|
||||
StartFlag = 1;
|
||||
if(PressFlag)
|
||||
{
|
||||
AutoBandTop1_SetPiecePlayMode(1, 0, 480);
|
||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||
AutoBandTop1_SetLoopMode(0, 1);
|
||||
}else
|
||||
{
|
||||
AutoBandTop1_SetPiecePlayMode(1,0,480);
|
||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||
}
|
||||
LOG_I("pick", "universal piece name=%s press=%u notes=%u chord=%u",
|
||||
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
||||
(unsigned)PressFlag, (unsigned)note_cnt, (unsigned)chord_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -941,9 +1147,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)
|
||||
{
|
||||
//char Info[10];
|
||||
//sprintf (Info, "%d %d", pos_offset,pitch_offset);
|
||||
//LCD_ShowString(2, 156+16, (const uint8_t*)Info, RED, WHITE, 16, 0);
|
||||
if (pos_offset < 1u || pos_offset > 21u)
|
||||
return;
|
||||
chord_type_index_map[pos_offset].PitchOffset = pitch_offset;
|
||||
}
|
||||
|
||||
|
|
@ -952,10 +1157,13 @@ void BT_Chord_offset_map(uint8_t pos_offset,uint8_t chord_offset)
|
|||
//char Info[20];
|
||||
STRING_MIDI *MIDI;
|
||||
const STRING_MIDI *ORGAN_MIDI;
|
||||
|
||||
uint8_t group = (pos_offset - 1) / 3; /* 1~3→0, 4~6→1, ..., 19~21→6 */
|
||||
uint8_t group;
|
||||
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){
|
||||
case 0x00:
|
||||
chord_APP_Local_offset = 0;
|
||||
|
|
@ -1280,9 +1488,10 @@ uint8_t GetChordNotesByType(uint8_t MIDI_Index,uint8_t type,uint8_t *buff)
|
|||
root = buff[0];
|
||||
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;
|
||||
while (buff[i] < root)
|
||||
for (guard = 0; guard < 2 && buff[i] < root; guard++)
|
||||
buff[i] += 12;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,8 +166,14 @@ extern uint8_t KEY_ID_1629;
|
|||
|
||||
extern bool powon;
|
||||
void Power_Key_Scan(void);
|
||||
void Power_Key_IsrTick(void);
|
||||
void Power_Key_FaultLoop(void);
|
||||
void Power_Key_WatchdogInit(void);
|
||||
void PowerOff_CutRails(void);
|
||||
void AutoPowerOff_Scan(void);
|
||||
void System_PowerOn(void);
|
||||
void System_RequestPowerOff(void);
|
||||
void System_PowerOff_Poll(void);
|
||||
extern uint8_t Led;
|
||||
extern bool PressFlag;
|
||||
|
||||
|
|
@ -211,6 +217,9 @@ uint8_t GUI_Item_AjustValue(GUI_SWITCH * Group, int8_t Dir);
|
|||
void GUI_Item_AjustLoopValue(GUI_SWITCH * Group, int8_t Dir);
|
||||
|
||||
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 TM1617_Handle(uint8_t key);
|
||||
|
|
|
|||
|
|
@ -19,10 +19,318 @@ static uint8_t pStoreBuffer[PRESET_BUFFER_SIZE];
|
|||
//static uint8_t* pStoreBuffer;
|
||||
#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)
|
||||
{
|
||||
MidiFifoItem_t midimsg;
|
||||
midimsg.msg[0] = 0xc0 | channel;
|
||||
LOG_I("REG", "pc ch%u prog=%d", (unsigned)channel, program);
|
||||
midimsg.msg[0] = 0xc0 | (channel & 0x0F);
|
||||
midimsg.msg[1] = program;
|
||||
midimsg.msg[2] = 0;
|
||||
midimsg.msglen = 2;
|
||||
|
|
@ -32,8 +340,10 @@ static void Func_CallBack_ProgramChange(int channel, int program)
|
|||
static void Func_CallBack_NoteOff(int channel,int key)
|
||||
{
|
||||
MidiFifoItem_t midimsg;
|
||||
midimsg.msg[0] = 0x80 | channel;
|
||||
midimsg.msg[1] = key;
|
||||
uint8_t flags = 0;
|
||||
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.msglen = 3;
|
||||
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
||||
|
|
@ -43,9 +353,19 @@ static void Func_CallBack_NoteOff(int channel,int key)
|
|||
static void Func_CallBack_NoteOn(int channel,int key,int vel)
|
||||
{
|
||||
MidiFifoItem_t midimsg;
|
||||
midimsg.msg[0] = 0x90 | channel;
|
||||
midimsg.msg[1] = key;
|
||||
midimsg.msg[2] = vel;
|
||||
int out_key;
|
||||
|
||||
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;
|
||||
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
||||
|
||||
|
|
@ -96,6 +416,31 @@ static const char *ToneFlashMapBank(uint32_t abs)
|
|||
|
||||
static uint32_t s_flash_rd_last_base = 0xFFFFFFFFu;
|
||||
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)
|
||||
{
|
||||
|
|
@ -135,6 +480,9 @@ static int Func_CallBack_ReadFlash(int address,int length,uint8_t* pOutput)
|
|||
//
|
||||
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_Init();
|
||||
AutoBandTop1_RegisterCallBack_NoteOn(Func_CallBack_NoteOn);
|
||||
|
|
@ -145,11 +493,15 @@ void App_Auto_Init(void)
|
|||
AutoBandTop1_RegisterReadFlashCallback(Func_CallBack_ReadFlash);
|
||||
m_presetCount = AutoBandTop1_GetPresetItemCount();
|
||||
|
||||
int ret = AutoBandTop1_LoadPresetItemFromFlash(0);
|
||||
int ret = App_Auto_LoadPresetFromFlash(0);
|
||||
if (ret != 0)
|
||||
{
|
||||
LOG_E("AUTO", "LoadPreset fail ret=%d addr=0x%08X map=%s",
|
||||
ret, (unsigned)ADDRESS, ToneFlashMapBank((uint32_t)ADDRESS));
|
||||
uint8_t hdr[16];
|
||||
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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,9 +8,14 @@
|
|||
#ifndef APP_AUTO_H_
|
||||
#define APP_AUTO_H_
|
||||
#include "mymidififo1.h"
|
||||
#include <stdint.h>
|
||||
|
||||
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;
|
||||
|
||||
#endif /* APP_AUTO_H_ */
|
||||
|
|
|
|||
19
UI/UI_Idle.c
|
|
@ -43,12 +43,6 @@ static void UI_ModeSelect_DrawButtons(void)
|
|||
LCD_WR_PIC_FROM_FLASH(UI0902_MODE_ROW_X, y,
|
||||
UI0902_MODE_ROW_W, UI0902_MODE_ROW_H,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -169,18 +163,19 @@ void IdleProcess(DisplayTaskMessage_Type msg)
|
|||
TM1629D_UpdateDisplay(0);
|
||||
StartTouchTask();
|
||||
StartScanTask();
|
||||
/* Dream 上电稍后可能忽略首包 SysEx:再发主音量 + 调音台(保持关机前设置) */
|
||||
/* Dream 上电稍后可能忽略首包 SysEx:再发主音量 + 调音台(保持关机前设置)。
|
||||
* 此时扫描任务已启动、模拟开关在轮询,不可现场重读 ADC(会串通道),
|
||||
* 统一用 MasterVolume_Reapply 取扫描任务维护的最近一次有效音量 */
|
||||
rt_thread_mdelay(400);
|
||||
vol = ADC_ReadChannel(ADC_CHANNEL_1)>>5;
|
||||
app_adc_sync_volume(vol);
|
||||
Send_volume(vol);
|
||||
MasterVolume_Reapply();
|
||||
LoadConfig();
|
||||
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, level, 1, WHITE, GRAY);
|
||||
break;
|
||||
|
||||
case MSG_ID_POWER_OFF:
|
||||
/* 关机:熄屏 + 关背光 */
|
||||
/* 无 Type-C:整机关机熄屏;有 Type-C 时随后 POWOFF_CHARG 会再亮充电图 */
|
||||
LCD_BLK_Clr();
|
||||
LCD_WR_REG(0x28); /* Display OFF */
|
||||
LCD_FillByColor(0, 0, 240, 320, BLACK);
|
||||
break;
|
||||
|
||||
|
|
@ -190,6 +185,8 @@ void IdleProcess(DisplayTaskMessage_Type msg)
|
|||
|
||||
case MSG_ID_TOUCH:
|
||||
{
|
||||
if (!powon)
|
||||
break; /* 软关机已停触摸;兜底勿进模式选择 */
|
||||
static uint32_t s_last_enter_ms;
|
||||
uint32_t now = rt_tick_get() * 1000U / RT_TICK_PER_SECOND;
|
||||
uint8_t hit = 0U;
|
||||
|
|
|
|||
|
|
@ -53,8 +53,33 @@ void DrawAllFaders(void)
|
|||
drv_nvm_save_to_flash();
|
||||
}
|
||||
|
||||
/* 0902-06:实心蓝色圆钮(勿用同心空心圆,会透出色条) */
|
||||
#define MIXER_KNOB_R 14
|
||||
/* 0902-06:圆钮定位半径 = 色条半宽;填充略大以盖住柱端抗锯齿白边(夹紧不画出柱外) */
|
||||
#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)
|
||||
{
|
||||
|
|
@ -62,9 +87,9 @@ static void DrawFaderSlider(Fader_t *f, uint16_t slider_y)
|
|||
uint16_t ly = f->y + f->h + 8;
|
||||
uint16_t cy = (uint16_t)(slider_y + MIXER_KNOB_R);
|
||||
char buf[8];
|
||||
LCD_FillCircle(cx, cy, MIXER_KNOB_R, COLOR_GRAD_MID);
|
||||
DrawMixerKnob(cx, cy);
|
||||
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),
|
||||
(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);
|
||||
|
|
@ -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)
|
||||
{
|
||||
uint16_t slider_h = (uint16_t)(MIXER_KNOB_R * 2);
|
||||
uint16_t fill_h = (f->val * f->h) / 10;
|
||||
if (fill_h > f->h) fill_h = f->h;
|
||||
uint16_t slider_y = f->y + f->h - fill_h - (slider_h / 2);
|
||||
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;
|
||||
uint16_t top_cy = (uint16_t)(f->y + MIXER_KNOB_R);
|
||||
uint16_t bot_cy = (uint16_t)(f->y + f->h - 1 - MIXER_KNOB_R);
|
||||
uint16_t cy = (uint16_t)(bot_cy - ((uint32_t)(bot_cy - top_cy) * f->val) / 10);
|
||||
return (uint16_t)(cy - MIXER_KNOB_R);
|
||||
}
|
||||
|
||||
int CheckTouchFader(uint16_t x, uint16_t y)
|
||||
|
|
@ -222,7 +244,7 @@ void UI_Mixer_Process(DisplayTaskMessage_Type msg)
|
|||
break;
|
||||
|
||||
case MSG_ID_EC_VOL:
|
||||
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, msg.HiByte, 1, WHITE, GRAY);
|
||||
/* 调音台子页无状态栏音量条(与返回键重叠);主音量已在 MainTask 下发 */
|
||||
ResetAutoPowerCount();
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,11 +180,11 @@ void Touch_Action(int8_t FLAG, uint8_t ID, uint8_t areaIndex,uint8_t value)
|
|||
UI_ApplyToneAddress();
|
||||
int ret = 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)
|
||||
ret = AutoBandTop1_LoadPresetItemFromFlash(ParamGuiData[EXPRESS_MODE_PARAM].Current);
|
||||
ret = App_Auto_LoadPresetFromFlash(ParamGuiData[EXPRESS_MODE_PARAM].Current);
|
||||
else
|
||||
ret = AutoBandTop1_LoadPresetItemFromFlash(ParamGuiData[SONG_MODE_PARAM].Current);
|
||||
ret = App_Auto_LoadPresetFromFlash(ParamGuiData[SONG_MODE_PARAM].Current);
|
||||
if (ret != 0)
|
||||
LOG_E("tone", "load preset failed tab=%d sw=%d ret=%d addr=0x%08X",
|
||||
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)
|
||||
{
|
||||
/* 万能模式第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)
|
||||
{
|
||||
mGuiData[ID].Current = value;
|
||||
AutoBandTop1_SetRunVar(mGuiData[ID].Current);
|
||||
}else
|
||||
{
|
||||
/* 触摸起奏前补发主音量,保证实际音量与图标一致 */
|
||||
MasterVolume_Reapply();
|
||||
AutoBandTop1_FillinWithIndex(value);
|
||||
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)
|
||||
break;
|
||||
StartFlag = 0;
|
||||
KEY_ID_1629 = 0; /* 清专业等残留指板键,拨片按当前调走走向 */
|
||||
PressFlag = 0;
|
||||
AutoBandTop1_Stop();
|
||||
UI_ReloadTonePreset(); /* 切换模式必须换库,避免专业仍播万能数据 */
|
||||
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_Y1, (uint16_t)(UI0902_HUB_CARD_Y1 + UI0902_HUB_CARD_H - 1),
|
||||
0, 0, GUI_SETING_SELECT3, NULL },
|
||||
/* 底栏四键 */
|
||||
{ 0, 69, 280, 319, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
|
||||
{ 70, 129, 280, 319, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
|
||||
{ 130, 184, 280, 319, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
|
||||
{ 185, 239, 280, 319, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
|
||||
/* 底栏四键:与 Touch_Areas 共用 UI0902_NAV_* 几何 */
|
||||
{ UI0902_NAV_X0(0), UI0902_NAV_X1(0), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_UNIVERSAL, 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 },
|
||||
{ UI0902_NAV_X0(2), UI0902_NAV_X1(2), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_EXPERT, 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))
|
||||
|
|
@ -124,6 +124,11 @@ void UI_SystemSet_Process(DisplayTaskMessage_Type msg)
|
|||
label_top();
|
||||
break;
|
||||
|
||||
case MSG_ID_EC_VOL:
|
||||
/* 主音量已在 MainTask 下发;此处只刷新状态栏音量条,保证与旋钮一致 */
|
||||
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, msg.HiByte, 1, WHITE, GRAY);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
183
UI/UI_global.c
|
|
@ -54,6 +54,7 @@ void CallUI_Idle(void) {
|
|||
|
||||
void CallUI_SongMode(void) {
|
||||
app_touch_suppress(500);
|
||||
MasterVolume_Reapply(); /* 进模式补发主音量,防开机 SysEx 丢失后实际音量与图标不符 */
|
||||
UI_SongMode_Init();
|
||||
CurrUIProcress = UI_SongMode_Process;
|
||||
app_log_set_ui_page("Song");
|
||||
|
|
@ -62,6 +63,7 @@ void CallUI_SongMode(void) {
|
|||
|
||||
void CallUI_ExpertMode(void) {
|
||||
app_touch_suppress(500);
|
||||
MasterVolume_Reapply();
|
||||
UI_ExpertMode_Init();
|
||||
// CurrUIProcress = UI_ExpertMode_Process;
|
||||
CurrUIProcress = UI_SongMode_Process;
|
||||
|
|
@ -71,6 +73,7 @@ void CallUI_ExpertMode(void) {
|
|||
|
||||
void CallUI_FreeMode(void) {
|
||||
app_touch_suppress(500);
|
||||
MasterVolume_Reapply();
|
||||
UI_FreeMode_Init();
|
||||
// CurrUIProcress = UI_FreeMode_Process;
|
||||
CurrUIProcress = UI_SongMode_Process;
|
||||
|
|
@ -167,11 +170,24 @@ void UI_ReloadTonePreset(void)
|
|||
/* 万能:和弦走向 */
|
||||
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);
|
||||
if (ret != 0)
|
||||
{
|
||||
uint8_t hdr[16];
|
||||
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;
|
||||
}
|
||||
/* 普通/专业:本地曲目或节奏类型 */
|
||||
|
|
@ -181,7 +197,7 @@ void UI_ReloadTonePreset(void)
|
|||
local->Max = (LOCAL_SONG_COUNT > 0) ? (uint16_t)(LOCAL_SONG_COUNT - 1) : 0;
|
||||
if (local->Current > local->Max)
|
||||
local->Current = local->Max;
|
||||
int ret = AutoBandTop1_LoadPresetItemFromFlash(local->Current);
|
||||
int ret = App_Auto_LoadPresetFromFlash(local->Current);
|
||||
if (ret != 0)
|
||||
LOG_E("tone", "load local song idx=%d failed ret=%d addr=0x%08X map=BIN2@0x%08X",
|
||||
local->Current, ret, (unsigned)ADDRESS,
|
||||
|
|
@ -201,7 +217,7 @@ void UI_ReloadTonePreset(void)
|
|||
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",
|
||||
ParamGuiData[SONG_MODE_PARAM].Current, ret,
|
||||
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)
|
||||
{
|
||||
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;
|
||||
uint8_t idx = Group->Current + 1;
|
||||
uint16_t c_top, c_bot;
|
||||
uint16_t x1, x2;
|
||||
|
||||
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(
|
||||
UI->x+25, row_y,
|
||||
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
|
||||
);
|
||||
|
||||
// 拼接序号 1. 2. 3.
|
||||
/* 序号+名称整体居中(约等于标注「右移7px」的视觉效果) */
|
||||
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)
|
||||
strcpy(Textstr_new, LocalSongNameGbk[Group->Current]);
|
||||
else
|
||||
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);
|
||||
//序号X = 中文起点 - 数字宽度 - 5,间<EFBC8C>?像素
|
||||
uint16_t num_x = string_start_x - num_w;
|
||||
// 绘制左侧数字序号
|
||||
LCD_ShowString(num_x, text_y, (const uint8_t *)NumBuf, UI->Text_fc, UI->Text_bc, UI->Text_size, 1);
|
||||
UI_DrawIndexedName_Mixed(x1, x2, text_y, NumBuf, Textstr_new,
|
||||
WHITE, BLACK, UI->Text_size);
|
||||
|
||||
/* 补画左右三角(几何与 label_ModeSelece_ui 一致) */
|
||||
{
|
||||
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];
|
||||
}
|
||||
|
||||
/* 左右三角由 show_Param 末尾统一补画(整行清除后必须重画) */
|
||||
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)
|
||||
|
|
@ -851,22 +928,17 @@ void label_TimbreSelece_ui(const LEBEL_UI *UI, GUI_SWITCH * Group)
|
|||
c_top, c_bot
|
||||
);
|
||||
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);
|
||||
uint32_t chinese_start_x = LCD_ShowChinese_AutoAlign_Gradient(
|
||||
UI->x + num_w,
|
||||
UI->x + UI->w,
|
||||
UI_DrawIndexedName_Grad(
|
||||
(uint16_t)(UI->x + 10),
|
||||
(uint16_t)(UI->x + UI->w - 10),
|
||||
text_y,
|
||||
(const uint8_t*)Textstr,
|
||||
UI->Text_Align,
|
||||
NumBuf,
|
||||
Textstr,
|
||||
UI->Text_fc,
|
||||
c_top, c_bot,
|
||||
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)
|
||||
{
|
||||
case GUI_TIMBRE_SELECT:
|
||||
|
|
@ -907,37 +979,32 @@ void UI_DrawSubPageHeader(const uint8_t *title_gbk)
|
|||
|
||||
void Draw_Bottom_Bar(uint8_t sel, uint8_t first_back)
|
||||
{
|
||||
/* 需求稿四键:万能 | 普通 | 专业 | 设置;first_back 时设置位仍高亮(返回用触控逻辑) */
|
||||
/* 四键合成图(图标+原稿文字);统一 32 高,文字底边对齐 */
|
||||
const uint8_t *icons[4];
|
||||
uint8_t ws[4], hs[4];
|
||||
/* 0904:四键中心约 40 / 100 / 160 / 210 */
|
||||
uint8_t ws[4];
|
||||
const uint16_t cx[4] = { 40, 100, 160, 210 };
|
||||
const uint8_t icon_h = 32;
|
||||
const uint16_t icon_y = 282;
|
||||
uint8_t i;
|
||||
|
||||
(void)first_back;
|
||||
/* 从段落钮底边之下清屏,勿切掉按钮底圆角(钮 y=231..264) */
|
||||
LCD_FillByColor(0, (uint16_t)(UI0902_SECTION_Y + UI0902_SECTION_BTN_H),
|
||||
240, 320, UI0902_BG_COLOR);
|
||||
/* mode.png:段落钮与底栏之间横线分隔(与顶栏同色同厚) */
|
||||
LCD_FillByColor(0, UI0902_SEP_LINE_Y, 240,
|
||||
(uint16_t)(UI0902_SEP_LINE_Y + UI0902_SEP_LINE_THICK),
|
||||
UI0902_SEP_LINE_COLOR);
|
||||
|
||||
if (sel == UI0902_NAV_UNIVERSAL) { icons[0] = gImage_Nav_Universal_Sel; ws[0] = 36; hs[0] = 28; }
|
||||
else { icons[0] = gImage_Nav_Universal_Not; ws[0] = 36; hs[0] = 28; }
|
||||
|
||||
if (sel == UI0902_NAV_NORMAL) { icons[1] = gImage_Nav_Normal_Sel; ws[1] = 35; hs[1] = 29; }
|
||||
else { icons[1] = gImage_Nav_Normal_Not; ws[1] = 35; hs[1] = 28; }
|
||||
|
||||
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; }
|
||||
icons[0] = (sel == UI0902_NAV_UNIVERSAL) ? gImage_Nav_Universal_Sel : gImage_Nav_Universal_Not;
|
||||
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;
|
||||
icons[3] = (sel == UI0902_NAV_SETTING) ? gImage_Nav_Setting_Sel : gImage_Nav_Setting_Not;
|
||||
ws[0] = ws[1] = ws[2] = 40;
|
||||
ws[3] = 22;
|
||||
|
||||
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 +1055,7 @@ void Draw_Tab_Section_Menu(const LEBEL_UI *UI, TAB_SECTION_INDEX select_tab)
|
|||
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_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 char *nums[4] = {"1", "2", "3", "4"};
|
||||
uint8_t i;
|
||||
|
|
@ -999,7 +1066,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;
|
||||
LCD_FillRoundRect(x, start_y, tab_w, tab_h, tab_r, bg);
|
||||
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],
|
||||
UI->Text_Align,
|
||||
WHITE,
|
||||
|
|
@ -1223,11 +1290,11 @@ void Draw_System_Item_List(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
|||
}
|
||||
break;
|
||||
case GUI_BLUETOOTH_SW:
|
||||
/* 素材:系统设置图标-11(白蓝牙符);Flash 槽名 RESTORE,尺寸 13x21 */
|
||||
/* 素材:系统设置图标-11(白蓝牙符)→ ICON_SET_BLUETOOTH 13x21 */
|
||||
LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12,
|
||||
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_RESTORE_H) / 2),
|
||||
UI0902_ICON_SYS_RESTORE_W, UI0902_ICON_SYS_RESTORE_H,
|
||||
UI0902_ICON_SYS_RESTORE_ADDR);
|
||||
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SET_BLUETOOTH_H) / 2),
|
||||
UI0902_ICON_SET_BLUETOOTH_W, UI0902_ICON_SET_BLUETOOTH_H,
|
||||
UI0902_ICON_SET_BLUETOOTH_ADDR);
|
||||
UI_DrawTriLeft(val_x1, chev_cy);
|
||||
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);
|
||||
|
|
@ -1254,9 +1321,9 @@ void Draw_System_Item_List(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
|||
WHITE, c_top, c_bot, value_sz);
|
||||
}else
|
||||
{
|
||||
/* 手册「不自动」;字库无「不」,界面用稿面「关机」 */
|
||||
/* 手册「不自动」;字库无「不」,界面显示「关闭」(字库已补「闭」) */
|
||||
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);
|
||||
}
|
||||
UI_DrawTriRight((uint16_t)(val_x2 - UI0902_TRI_R_W), chev_cy);
|
||||
|
|
@ -1264,13 +1331,17 @@ void Draw_System_Item_List(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
|||
drv_nvm_save_to_flash();
|
||||
break;
|
||||
case GUI_RESTORE:
|
||||
/* 逆时针箭头:当前在 VERSION 槽(19x18);RESTORE 槽曾误为蓝牙符 */
|
||||
/* 素材:系统设置图标-13(逆时针恢复)→ ICON_SYS_RESTORE 19x18 */
|
||||
LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12,
|
||||
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_VERSION_H) / 2),
|
||||
UI0902_ICON_SYS_VERSION_W, UI0902_ICON_SYS_VERSION_H,
|
||||
UI0902_ICON_SYS_VERSION_ADDR);
|
||||
/* 右三角与蓝牙/自动关机同一右缘(val_x2) */
|
||||
UI_DrawTriRight((uint16_t)(val_x2 - UI0902_TRI_R_W), chev_cy);
|
||||
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_RESTORE_H) / 2),
|
||||
UI0902_ICON_SYS_RESTORE_W, UI0902_ICON_SYS_RESTORE_H,
|
||||
UI0902_ICON_SYS_RESTORE_ADDR);
|
||||
/* 0902-07:右侧为进入下级页的细 chevron(设置图标-03),勿用实心三角 */
|
||||
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;
|
||||
default: break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,19 +173,25 @@ void CallUI_RestoreSelect(void);
|
|||
#define UI0902_NAV_EXPERT 2 /* 底栏:专业模式 */
|
||||
#define UI0902_NAV_SETTING 3 /* 底栏:设置 */
|
||||
#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_MODE UI0902_NAV_EXPERT
|
||||
|
||||
/* 底部图标实际内容(生成脚本曾把 万能/普通/专业/设置 误命名为 Setting/Mixer/Mode) */
|
||||
#define gImage_Nav_Universal_Not gImage_UI0902_TabSetting_Sel_36x28 /* 底部-10 白 */
|
||||
#define gImage_Nav_Universal_Sel gImage_UI0902_TabMode_Sel_36x28 /* 底部-14 蓝 */
|
||||
#define gImage_Nav_Normal_Not gImage_UI0902_TabSetting_Not_35x28 /* 底部-11 白 */
|
||||
#define gImage_Nav_Normal_Sel gImage_UI0902_TabMode_Not_35x29 /* 底部-15 蓝 */
|
||||
#define gImage_Nav_Expert_Not gImage_UI0902_TabMixer_Sel_35x27 /* 底部-12 白 */
|
||||
#define gImage_Nav_Expert_Sel gImage_UI0902_TabBack_Sel_35x27 /* 底部-16 蓝 */
|
||||
#define gImage_Nav_Setting_Not gImage_UI0902_TabMixer_Not_18x28 /* 底部-13 白 */
|
||||
#define gImage_Nav_Setting_Sel gImage_UI0902_TabBack_Not_18x28 /* 底部-17 蓝 */
|
||||
/* 底部图标:0909 加大图标 + 原稿文字条(合成图);生成脚本曾误命名 Setting/Mixer/Mode */
|
||||
#define gImage_Nav_Universal_Not gImage_UI0902_TabSetting_Sel_40x32 /* 底部-10 白 */
|
||||
#define gImage_Nav_Universal_Sel gImage_UI0902_TabMode_Sel_40x32 /* 底部-14 蓝 */
|
||||
#define gImage_Nav_Normal_Not gImage_UI0902_TabSetting_Not_40x32 /* 底部-11 白 */
|
||||
#define gImage_Nav_Normal_Sel gImage_UI0902_TabMode_Not_40x32 /* 底部-15 蓝 */
|
||||
#define gImage_Nav_Expert_Not gImage_UI0902_TabMixer_Sel_40x32 /* 底部-12 白 */
|
||||
#define gImage_Nav_Expert_Sel gImage_UI0902_TabBack_Sel_40x32 /* 底部-16 蓝 */
|
||||
#define gImage_Nav_Setting_Not gImage_UI0902_TabMixer_Not_22x32 /* 底部-13 白 */
|
||||
#define gImage_Nav_Setting_Sel gImage_UI0902_TabBack_Not_22x32 /* 底部-17 蓝 */
|
||||
/* 旧几何电池(Draw_Battery_Icon)最左起点;0902 图电池用 Draw_Status_Battery_Icon */
|
||||
#define UI_STATUS_BATTERY_X 150
|
||||
#define UI_STATUS_BATTERY_H 11
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ const char CHS8Table[][2] = {
|
|||
{0xC0,0xAB}, // 阔
|
||||
{0xCC,0xEC}, // 天
|
||||
{0xBF,0xD5}, // 空
|
||||
{0xB1,0xD5}, // 闭
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -125,6 +126,7 @@ const char CHS9Table[][2] = {
|
|||
{0xC0,0xAB}, // 阔
|
||||
{0xCC,0xEC}, // 天
|
||||
{0xBF,0xD5}, // 空
|
||||
{0xB1,0xD5}, // 闭
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -191,6 +193,7 @@ const char CHS11Table[][2] = {
|
|||
{0xC0,0xAB}, // 阔
|
||||
{0xCC,0xEC}, // 天
|
||||
{0xBF,0xD5}, // 空
|
||||
{0xB1,0xD5}, // 闭
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -256,6 +259,7 @@ const char CHS12Table[][2] = {
|
|||
{0xC0,0xAB}, // 阔
|
||||
{0xCC,0xEC}, // 天
|
||||
{0xBF,0xD5}, // 空
|
||||
{0xB1,0xD5}, // 闭
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -449,6 +453,7 @@ const char CHS13Table[][2] = {
|
|||
{0xC0,0xAB}, // 阔
|
||||
{0xCC,0xEC}, // 天
|
||||
{0xBF,0xD5}, // 空
|
||||
{0xB1,0xD5}, // 闭
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -641,6 +646,7 @@ const char CHS15Table[][2] = {
|
|||
{0xC0,0xAB}, // 阔
|
||||
{0xCC,0xEC}, // 天
|
||||
{0xBF,0xD5}, // 空
|
||||
{0xB1,0xD5}, // 闭
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -837,6 +843,7 @@ const char CHS16Table[][2] = {
|
|||
{0xC0,0xAB}, // 阔
|
||||
{0xCC,0xEC}, // 天
|
||||
{0xBF,0xD5}, // 空
|
||||
{0xB1,0xD5}, // 闭
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -871,6 +878,7 @@ const char CHS24Table[][2] = {
|
|||
{0xB6, 0xA8},// 认
|
||||
{0xB7, 0xD6},// 分
|
||||
{0xD6, 0xD3},// 钟
|
||||
{0xB1,0xD5}, // 闭
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -930,6 +938,7 @@ const unsigned char Chinese_font_8[][8] = {
|
|||
{0x1F,0x41,0x89,0x89,0xA9,0xDD,0xD7,0xC1},/*"阔",52*/
|
||||
{0x7E,0x00,0x00,0xFF,0x18,0x00,0x00,0x81},/*"天",53*/
|
||||
{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] = {
|
||||
|
|
@ -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*/
|
||||
{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*/
|
||||
{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] = {
|
||||
|
|
@ -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*/
|
||||
{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*/
|
||||
{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] = {
|
||||
|
|
@ -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*/
|
||||
{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*/
|
||||
{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*/
|
||||
{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*/
|
||||
{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*/
|
||||
{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*/
|
||||
{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] = {
|
||||
|
|
@ -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*/
|
||||
{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*/
|
||||
{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] = {
|
||||
|
|
@ -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,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*/
|
||||
{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*/
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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_Bluetooth_11x17[374];
|
||||
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_Not_35x28[1960];
|
||||
extern const unsigned char gImage_UI0902_TabMixer_Sel_35x27[1890];
|
||||
extern const unsigned char gImage_UI0902_TabMixer_Not_18x28[1008];
|
||||
extern const unsigned char gImage_UI0902_TabSetting_Sel_40x32[2560];
|
||||
extern const unsigned char gImage_UI0902_TabSetting_Not_40x32[2560];
|
||||
extern const unsigned char gImage_UI0902_TabMixer_Sel_40x32[2560];
|
||||
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_TabMode_Sel_36x28[2016];
|
||||
extern const unsigned char gImage_UI0902_TabMode_Not_35x29[2030];
|
||||
extern const unsigned char gImage_UI0902_TabBack_Sel_35x27[1890];
|
||||
extern const unsigned char gImage_UI0902_TabBack_Not_18x28[1008];
|
||||
extern const unsigned char gImage_UI0902_TabMode_Sel_40x32[2560];
|
||||
extern const unsigned char gImage_UI0902_TabMode_Not_40x32[2560];
|
||||
extern const unsigned char gImage_UI0902_TabBack_Sel_40x32[2560];
|
||||
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_BtnPlus_23x22[1012];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// File: w25q128.c
|
||||
#include "w25q128.h"
|
||||
#include "wk_system.h"
|
||||
#include "rtthread.h"
|
||||
|
||||
/* W25Q128 挂在 SPI1 上(PA5=SCK, PA6=MISO, PA7=MOSI),CS 用 PA4 GPIO 控制 */
|
||||
#define W25Q128_SPI SPI1
|
||||
|
|
@ -82,6 +83,12 @@ static void W25Q128_WaitBusy(void)
|
|||
W25Q128_ReadWriteByte(W25X_ReadStatusReg1); // 发送读状态寄存器命令
|
||||
do {
|
||||
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位清零
|
||||
SPI_CS_HIGH();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,3 +37,22 @@ void SendMidiDataToDreamDSP(uint8_t* buff, uint16_t 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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,4 +11,8 @@ void SendVolumeChangeMidiEvent(uint8_t channel, uint8_t CC);
|
|||
void SendMidiDataToDreamDSP(uint8_t* buff, uint16_t cnt);
|
||||
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__ */
|
||||
|
|
|
|||
|
|
@ -220,7 +220,8 @@ const STRING_MIDI *Chord_Midi_Table[7] =
|
|||
const STRING_MIDI *OrganChord_Midi_Table[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:24尼龙 / 25钢弦 / 27清音 / 30失真 / 33 Finger贝斯 */
|
||||
const uint8_t Timbre_index[5] = {0x18, 0x19, 0x1B, 0x1E, 0x21};
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -663,7 +663,7 @@
|
|||
<name>BUILDACTION</name>
|
||||
<archiveVersion>1</archiveVersion>
|
||||
<data>
|
||||
<prebuild></prebuild>
|
||||
<prebuild>python "$PROJ_DIR$\..\..\tools\gen_git_user_fw_ver.py"</prebuild>
|
||||
<postbuild></postbuild>
|
||||
</data>
|
||||
</settings>
|
||||
|
|
@ -1650,7 +1650,7 @@
|
|||
<name>BUILDACTION</name>
|
||||
<archiveVersion>1</archiveVersion>
|
||||
<data>
|
||||
<prebuild></prebuild>
|
||||
<prebuild>python "$PROJ_DIR$\..\..\tools\gen_git_user_fw_ver.py"</prebuild>
|
||||
<postbuild></postbuild>
|
||||
</data>
|
||||
</settings>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
/* Auto-generated by tools/pack_extflash_tone_0903.py — do not hand-edit. */
|
||||
/* 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. */
|
||||
#define EXTFLASH_TONE_RES_VER "0903"
|
||||
#define EXTFLASH_TONE_RES_VER "0914"
|
||||
#define EXTFLASH_TONE_RES_VER_MAJOR 0
|
||||
#define EXTFLASH_TONE_RES_VER_MINOR 9
|
||||
#define EXTFLASH_TONE_RES_VER_PATCH 3
|
||||
#define EXTFLASH_TONE_RES_VER_PATCH 14
|
||||
|
||||
#define EXTFLASH_LOGO_ADDR 0x00000000UL
|
||||
#define EXTFLASH_LOGO_SIZE 52080UL
|
||||
|
|
@ -18,13 +18,13 @@
|
|||
#define EXTFLASH_CHARGING_H 190
|
||||
|
||||
#define EXTFLASH_BIN1_RHYTHM_ADDR 0x0001B8F0UL
|
||||
#define EXTFLASH_BIN1_RHYTHM_SIZE 530317UL
|
||||
#define EXTFLASH_BIN1_RHYTHM_SIZE 509151UL
|
||||
|
||||
#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_SIZE 31854UL
|
||||
#define EXTFLASH_BIN3_UNIVERSAL_ADDR 0x000A7437UL
|
||||
#define EXTFLASH_BIN3_UNIVERSAL_SIZE 33815UL
|
||||
|
||||
/* Convenience aliases used by UI ADDRESS */
|
||||
#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_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 EXTFLASH_TONE_PACK_END 0x000AEE1AUL
|
||||
#define EXTFLASH_TONE_PACK_SIZE 716314UL
|
||||
#define EXTFLASH_TONE_PACK_END 0x000AF84EUL
|
||||
#define EXTFLASH_TONE_PACK_SIZE 718926UL
|
||||
#define UI0902_RES_BASE_SAFE_GAP (0x00100000UL - EXTFLASH_TONE_PACK_END)
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
void USART2_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=队列空 */
|
||||
uint8_t USART2_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);
|
||||
uint32_t wk_tmr6_tick_get(void);
|
||||
|
||||
/* TMR7 1ms:关机键看门狗(优先级高于 TMR6,主循环卡死仍可关) */
|
||||
void wk_tmr7_pwrkey_init(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@
|
|||
|
||||
/* 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 */
|
||||
|
||||
/* private typedef -----------------------------------------------------------*/
|
||||
|
|
@ -110,16 +112,7 @@ void NMI_Handler(void)
|
|||
*/
|
||||
void MemManage_Handler(void)
|
||||
{
|
||||
/* add user code begin MemoryManagement_IRQ 0 */
|
||||
|
||||
/* 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 */
|
||||
}
|
||||
Power_Key_FaultLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -129,16 +122,7 @@ void MemManage_Handler(void)
|
|||
*/
|
||||
void BusFault_Handler(void)
|
||||
{
|
||||
/* add user code begin BusFault_IRQ 0 */
|
||||
|
||||
/* 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 */
|
||||
}
|
||||
Power_Key_FaultLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -148,16 +132,7 @@ void BusFault_Handler(void)
|
|||
*/
|
||||
void UsageFault_Handler(void)
|
||||
{
|
||||
/* add user code begin UsageFault_IRQ 0 */
|
||||
|
||||
/* 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 */
|
||||
}
|
||||
Power_Key_FaultLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -241,6 +216,15 @@ void TMR6_GLOBAL_IRQHandler(void)
|
|||
//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 end 1 */
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ int main(void)
|
|||
drv_nvm_init();
|
||||
TaskInit();
|
||||
timer_init();
|
||||
/* 关机键看门狗:TMR7 + HardFault hook,与主循环/伴奏 ISR 解耦 */
|
||||
Power_Key_WatchdogInit();
|
||||
InitMenuUI();
|
||||
BSP_HT7178PowerEnable (1);
|
||||
BSP_ChargerEnable(1);
|
||||
|
|
@ -105,6 +107,7 @@ int main(void)
|
|||
/* add user code begin 3 */
|
||||
Power_Key_Scan();
|
||||
AutoPowerOff_Scan();
|
||||
System_PowerOff_Poll();
|
||||
LCD_Dump_Poll();
|
||||
#if !DEBUG_LCD_DUMP
|
||||
app_log_poll();
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
|
||||
#define BUFF_SIZE 512
|
||||
|
||||
/* 纯比较宏:参数必须是普通局部变量(非 volatile),
|
||||
避免同一表达式多次访问 volatile 触发 Pa082 且逻辑不稳 */
|
||||
/* ?????????????????????????????? volatile????
|
||||
?????????????????? volatile ???? Pa082 ????????? */
|
||||
#define IS_FULL(head, tail) ((((head) + 1) % BUFF_SIZE) == (tail))
|
||||
#define IS_EMPTY(head, tail) ((head) == (tail))
|
||||
|
||||
/* ==================== 环形队列结构体 ==================== */
|
||||
/* ==================== ???????????? ==================== */
|
||||
|
||||
/* 一个串口实例的环形队列(收发各一) */
|
||||
/* ????????????????????????????? */
|
||||
typedef struct
|
||||
{
|
||||
uint8_t rx_buff[BUFF_SIZE];
|
||||
|
|
@ -21,14 +21,14 @@ typedef struct
|
|||
volatile uint16_t tx_head;
|
||||
volatile uint16_t tx_tail;
|
||||
|
||||
usart_type *usart; /* 关联的串口外设(USART2 / UART4) */
|
||||
usart_type *usart; /* ??????????????USART2 / UART4?? */
|
||||
} uart_ring_t;
|
||||
|
||||
/* USART2 和 UART4 各一个实例 */
|
||||
/* USART2 ?? UART4 ???????? */
|
||||
static uart_ring_t uart2;
|
||||
static uart_ring_t uart4;
|
||||
|
||||
/* 初始化:关联串口外设(由 wk_usart2_init / wk_uart4_init 调用) */
|
||||
/* ?????????????????????? wk_usart2_init / wk_uart4_init ????? */
|
||||
void uart_ring_usart2_init(void)
|
||||
{
|
||||
uart2.usart = USART2;
|
||||
|
|
@ -39,19 +39,19 @@ void uart_ring_uart4_init(void)
|
|||
uart4.usart = UART4;
|
||||
}
|
||||
|
||||
/* ==================== 通用入队/出队(带实例参数) ==================== */
|
||||
/* ==================== ??????/???????????????? ==================== */
|
||||
|
||||
/* 入队(ISR 调用)。先快照 volatile 值再操作,避免 Pa082 */
|
||||
/* ????ISR ???????????? volatile ???????????? Pa082 */
|
||||
static void ring_rx_push(uart_ring_t *rb, uint8_t c)
|
||||
{
|
||||
uint16_t h = rb->rx_head;
|
||||
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_head = (h + 1) % BUFF_SIZE;
|
||||
}
|
||||
|
||||
/* 出队(业务线程调用) */
|
||||
/* ??????????????? */
|
||||
static uint8_t ring_rx_pop(uart_ring_t *rb, uint8_t *c)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/* 写入 tx 队列(调用前应已关中断,防止 ISR 同时读) */
|
||||
/* ???? tx ??????????????????????? ISR ?????? */
|
||||
static void ring_tx_write(uart_ring_t *rb, uint8_t *data, uint16_t len)
|
||||
{
|
||||
uint16_t h = rb->tx_head;
|
||||
uint16_t t = rb->tx_tail;
|
||||
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];
|
||||
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)
|
||||
{
|
||||
rt_base_t level = rt_hw_interrupt_disable();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/* USART2 发送(对外,midi_send.c 等在用) */
|
||||
/* USART2 ?????????midi_send.c ??????? */
|
||||
void USART2_SendData(uint8_t *data, uint16_t len)
|
||||
{
|
||||
uart_ring_send(&uart2, data, len);
|
||||
}
|
||||
|
||||
/* UART4 发送(对外) */
|
||||
/* UART4 ????????? */
|
||||
void USART4_SendData(uint8_t *data, uint16_t 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)
|
||||
{
|
||||
return ring_rx_pop(&uart2, c);
|
||||
}
|
||||
|
||||
/* UART4 接收出队(对外) */
|
||||
/* UART4 ???????????? */
|
||||
uint8_t UART4_RxPop(uint8_t *c)
|
||||
{
|
||||
return ring_rx_pop(&uart4, c);
|
||||
}
|
||||
|
||||
/* ==================== 串口中断 ==================== */
|
||||
/* ==================== ???????? ==================== */
|
||||
|
||||
static void uart_ring_isr(uart_ring_t *rb)
|
||||
{
|
||||
/* 接收:收到字节 → 入 rx 队列 */
|
||||
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);
|
||||
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)
|
||||
{
|
||||
uint16_t h = rb->tx_head;
|
||||
|
|
@ -140,11 +155,10 @@ static void uart_ring_isr(uart_ring_t *rb)
|
|||
}
|
||||
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)
|
||||
usart_flag_clear(rb->usart, USART_ROERR_FLAG);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,3 +174,16 @@ void wk_tmr6_1ms_init(void)
|
|||
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 "git_user_fw_ver.h"
|
||||
|
||||
|
||||
// Э<><D0AD>֡ͷ/֡β
|
||||
|
|
@ -25,7 +26,7 @@ typedef enum
|
|||
UART4_RCV_BUFF_MIDIEND
|
||||
} 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>
|
||||
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>
|
||||
// ==============================
|
||||
//static void handleDevDisconnect(uint8_t *data);
|
||||
//static void handleDevConnect(uint8_t *data);
|
||||
//static void handleDevName(uint8_t *data);
|
||||
//static void handleFwMainVer(uint8_t *data);
|
||||
//static void handleSoundVer(uint8_t *data);
|
||||
//static void handleUIVer(uint8_t *data);
|
||||
//static void handleOtherInfo(uint8_t *data);
|
||||
//static void handleDevCode(uint8_t *data);
|
||||
//static void handleAutoPowerOff(uint8_t *data);
|
||||
static void handleDevDisconnect(uint8_t *data);
|
||||
static void handleDevConnect(uint8_t *data);
|
||||
static void handleDevName(uint8_t *data);
|
||||
static void handleFwMainVer(uint8_t *data);
|
||||
static void handleSoundVer(uint8_t *data);
|
||||
static void handleUIVer(uint8_t *data);
|
||||
static void handleOtherInfo(uint8_t *data);
|
||||
static void handleDevCode(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 handlePitchOffset(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 handleBPM(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 handleLED5(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 handleInterlude(uint8_t *data);
|
||||
|
|
@ -98,52 +101,54 @@ static void handleSectionD(uint8_t *data);
|
|||
*************************************************/
|
||||
static const BLE_SysExCmdItem bleSysExCmdTable[] =
|
||||
{
|
||||
// //======== 1. <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ 0x01 ========
|
||||
// {{0x01, 0x00}, handleDevDisconnect}, // <20>Ͽ<EFBFBD><CFBF>豸
|
||||
// {{0x01, 0x01}, handleDevConnect}, // <20><><EFBFBD><EFBFBD><EFBFBD>豸
|
||||
// {{0x01, 0x02}, handleDevName}, // <20><>ȡ<EFBFBD>豸<EFBFBD><E8B1B8>
|
||||
// {{0x01, 0x03}, handleFwMainVer}, // <20>̼<EFBFBD><CCBC><EFBFBD><EFBFBD>汾
|
||||
// {{0x01, 0x04}, handleSoundVer}, // <20><>Դ<EFBFBD>汾
|
||||
// {{0x01, 0x05}, handleUIVer}, // UI<55>汾
|
||||
// {{0x01, 0x06}, handleOtherInfo}, // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
|
||||
// {{0x01, 0x07}, handleDevCode}, // <20>豸<EFBFBD><E8B1B8><EFBFBD><EFBFBD>
|
||||
// {{0x01, 0x0A}, handleAutoPowerOff}, // <20>Զ<EFBFBD><D4B6>ػ<EFBFBD>
|
||||
//
|
||||
// //======== 2. ӳ<><D3B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 0x02 ========
|
||||
{{0x02, 0x01}, handleReadRhythmMap}, // <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD>/<2F><><EFBFBD><EFBFBD>ӳ<EFBFBD><D3B3><EFBFBD>
|
||||
{{0x02, 0x02}, handlePitchOffset}, // Pitchƫ<68><C6AB>
|
||||
{{0x02, 0x03}, handleChordOffset}, // Chordƫ<64><C6AB>
|
||||
// {{0x02, 0x04}, handleResetChordMap}, // <20><><EFBFBD>ú<EFBFBD><C3BA><EFBFBD>ӳ<EFBFBD><D3B3>Ĭ<EFBFBD><C4AC>ֵ
|
||||
/*======== 1. device info 0x01 ========*/
|
||||
{{0x01, 0x00}, handleDevDisconnect}, /* disconnect */
|
||||
{{0x01, 0x01}, handleDevConnect}, /* connect */
|
||||
{{0x01, 0x02}, handleDevName}, /* device name */
|
||||
{{0x01, 0x03}, handleFwMainVer}, /* fw main version */
|
||||
{{0x01, 0x04}, handleSoundVer}, /* sound version */
|
||||
{{0x01, 0x05}, handleUIVer}, /* UI version */
|
||||
{{0x01, 0x06}, handleOtherInfo}, /* other info */
|
||||
{{0x01, 0x07}, handleDevCode}, /* device code (96bit UID) */
|
||||
{{0x01, 0x0A}, handleAutoPowerOff}, /* auto power-off read (minutes) */
|
||||
{{0x01, 0x0C}, handleUserFwVer}, /* user fw version */
|
||||
{{0x01, 0x11}, handleAutoPowerOffSet}, /* auto power-off set (minutes) */
|
||||
{{0x01, 0x0F}, handleFwCode}, /* fw code (MIDI/BLE-safe; replaces 01 FF) */
|
||||
{{0x01, 0xFF}, handleFwCode}, /* fw code alias (raw UART only; 0xFF illegal in BLE-MIDI SysEx) */
|
||||
|
||||
//======== 3. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>д 0x03 ========
|
||||
// {{0x03, 0x04}, handleRhythmStyle}, // <20><>ȡ/<2F><><EFBFBD>ý<EFBFBD><C3BD><EFBFBD><EFBFBD><EFBFBD>
|
||||
{{0x03, 0x05}, handleStringTimbre}, // <20><>ȡ/<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɫ
|
||||
{{0x03, 0x06}, handleBPM}, // <20><>ȡ/<2F><><EFBFBD><EFBFBD>BPM
|
||||
{{0x03, 0x07}, handleTranspose}, // <20><>ȡ/<2F><><EFBFBD><EFBFBD><EFBFBD>Ƶ<EFBFBD>
|
||||
/*======== 2. chord map 0x02 ========*/
|
||||
{{0x02, 0x01}, handleReadRhythmMap}, /* read chord/pitch map */
|
||||
{{0x02, 0x02}, handlePitchOffset}, /* pitch offset */
|
||||
{{0x02, 0x03}, handleChordOffset}, /* chord offset */
|
||||
{{0x02, 0x04}, handleResetChordMap}, /* write whole map (default reset) */
|
||||
|
||||
//======== 4. <20><><EFBFBD><EFBFBD>/LED<45><44><EFBFBD><EFBFBD> 0x04 ========
|
||||
{{0x04, 0x00}, handleLED0}, // <20><>1<EFBFBD><31>LED
|
||||
{{0x04, 0x01}, handleLED1}, // <20><>2<EFBFBD><32>LED
|
||||
{{0x04, 0x02}, handleLED2}, // <20><>3<EFBFBD><33>LED
|
||||
{{0x04, 0x03}, handleLED3}, // <20><>4<EFBFBD><34>LED
|
||||
{{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>
|
||||
/*======== 3. guitar params 0x03 ========*/
|
||||
{{0x03, 0x04}, handleRhythmStyle}, /* rhythm style r/w + user list */
|
||||
{{0x03, 0x05}, handleStringTimbre}, /* string timbre r/w */
|
||||
{{0x03, 0x06}, handleBPM}, /* BPM r/w */
|
||||
{{0x03, 0x07}, handleTranspose}, /* transpose r/w */
|
||||
|
||||
// //======== 5. <20><>λ/<2F>ػ<EFBFBD> 0x05 ========
|
||||
// {{0x05, 0x00}, handleDeviceReset}, // <20>豸<EFBFBD><E8B1B8>λ
|
||||
//
|
||||
// //======== 6. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת 0x06 ========
|
||||
{{0x06, 0x01}, handleIntro}, // ǰ<><C7B0>
|
||||
{{0x06, 0x02}, handleInterlude}, // <20><><EFBFBD><EFBFBD>
|
||||
{{0x06, 0x03}, handleOutro}, // β<><CEB2>
|
||||
{{0x04, 0x07}, handleEnd}, // end
|
||||
{{0x06, 0x05}, handleSectionA}, // A<><41>
|
||||
{{0x06, 0x06}, handleSectionB}, // B<><42>
|
||||
{{0x06, 0x07}, handleSectionC}, // C<><43>
|
||||
{{0x06, 0x08}, handleSectionD}, // D<><44>
|
||||
/*======== 4. play / LED 0x04 ========*/
|
||||
{{0x04, 0x00}, handleLED0}, /* LED 1 */
|
||||
{{0x04, 0x01}, handleLED1}, /* LED 2 */
|
||||
{{0x04, 0x02}, handleLED2}, /* LED 3 */
|
||||
{{0x04, 0x03}, handleLED3}, /* LED 4 */
|
||||
{{0x04, 0x04}, handleLED4}, /* LED 5 */
|
||||
{{0x04, 0x05}, handleLED5}, /* LED 6 */
|
||||
{{0x04, 0x06}, handleLED6}, /* LED 7 */
|
||||
{{0x04, 0x07}, handleEnd}, /* end / stop play */
|
||||
|
||||
/*======== 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>
|
||||
|
|
@ -158,14 +163,16 @@ static const BLE_SysExCmdItem bleSysExCmdTable[] =
|
|||
*/
|
||||
void processBLESysEXData(uint8_t* data, uint8_t cnt)
|
||||
{
|
||||
//ResetAutoPowerCount();
|
||||
// <20><><EFBFBD><EFBFBD>֡У<D6A1><D0A3>
|
||||
// ֡У<D6A1><D0A3>
|
||||
if (data[0] != FRAME_SYS_HEAD || data[cnt - 1] != FRAME_SYS_TAIL || cnt > UART4_PROCESS_BUFF_SIZE || data[1] != 0x60)
|
||||
{
|
||||
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]};
|
||||
|
||||
// <20><><EFBFBD><EFBFBD>ָ<EFBFBD><D6B8><EFBFBD>ƥ<EFBFBD><C6A5>
|
||||
|
|
@ -195,6 +202,7 @@ void UART4_Data_Process(volatile uint8_t* data)
|
|||
last_tick = now;
|
||||
memset(UART4_Process_Buff, 0, sizeof(UART4_Process_Buff));
|
||||
UART4_RCV_cnt = 0;
|
||||
UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
||||
}
|
||||
|
||||
switch (UART4_RCV_Status)
|
||||
|
|
@ -251,7 +259,6 @@ void UART4_Data_Process(volatile uint8_t* data)
|
|||
}
|
||||
else
|
||||
{
|
||||
// <20>Ƿ<EFBFBD><C7B7>ֽڣ<D6BD><DAA3><EFBFBD>λ
|
||||
UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
||||
}
|
||||
break;
|
||||
|
|
@ -346,8 +353,9 @@ static void handleTranspose(uint8_t *data)
|
|||
{
|
||||
case 0:
|
||||
{
|
||||
uint8_t ReturnTranspose[8] = {0xF0,0x60,0x03,0x07,0x00,0x00,0x00,0xF7};
|
||||
ReturnTranspose[5] = mGuiData[GUI_TRANSPOSE].Current;
|
||||
/* doc: F0 60 03 07 00 <val> F7 (7 bytes total) */
|
||||
uint8_t ReturnTranspose[7] = {0xF0,0x60,0x03,0x07,0x00,0x00,0xF7};
|
||||
ReturnTranspose[5] = (uint8_t)mGuiData[GUI_TRANSPOSE].Current;
|
||||
USART4_SendData(ReturnTranspose,sizeof(ReturnTranspose));
|
||||
}
|
||||
break;
|
||||
|
|
@ -472,23 +480,40 @@ static void handleLED6(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)
|
||||
{
|
||||
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];
|
||||
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;
|
||||
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;
|
||||
}
|
||||
for(uint8_t i = 25;i < 46;i ++)
|
||||
for (i = 25; i < 46; i++)
|
||||
{
|
||||
switch (chord_type_index_map[i - 24].type)
|
||||
{
|
||||
|
|
@ -519,6 +544,9 @@ static void handleReadRhythmMap(uint8_t *data)
|
|||
case 8:
|
||||
chord_map = 11;
|
||||
break;
|
||||
default:
|
||||
chord_map = 0;
|
||||
break;
|
||||
}
|
||||
ReadChordMap[i] = chord_map;
|
||||
}
|
||||
|
|
@ -546,7 +574,7 @@ static void handleEnd(uint8_t *data)
|
|||
// TM1629D_AllLedOn(LED_COLOR_G);
|
||||
// TM1629D_UpdateDisplay(0);
|
||||
// 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)
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/* ======================================================================
|
||||
* 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): 61caeb7407b978f03d57fac49e0e7142fca755c1
|
||||
* branch: develop → develop
|
||||
* 01 0C wire: develop_61caeb*
|
||||
*/
|
||||
#ifndef GIT_USER_FW_VER_H
|
||||
#define GIT_USER_FW_VER_H
|
||||
|
||||
#define GIT_COMMIT_ID_FULL "61caeb7407b978f03d57fac49e0e7142fca755c1"
|
||||
#define GIT_BRANCH_NAME "develop"
|
||||
#define GIT_COMMIT_SHORT6 "61caeb"
|
||||
#define GIT_DIRTY (1)
|
||||
#define GIT_BUILD_ID "develop_61caeb*"
|
||||
#define GIT_BUILD_ID_LEN 15u
|
||||
|
||||
#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 TaskMainThread_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];
|
||||
|
||||
|
||||
|
|
@ -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_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;
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -129,7 +132,6 @@ void TaskBTRecvThread_entry(void* parameter)
|
|||
{
|
||||
while(UART4_RxPop(&c))
|
||||
{
|
||||
//解析函数
|
||||
UART4_Data_Process(&c);
|
||||
}
|
||||
}
|
||||
|
|
@ -238,6 +240,35 @@ void StartTask(void)
|
|||
/* 扫描任务可能已由 StartScanTask 拉起;重复 startup 会被 RT-Thread 忽略 */
|
||||
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)
|
||||
{
|
||||
rt_thread_init(&TaskBTHandleThread,
|
||||
|
|
@ -334,18 +365,12 @@ void StopFullTask(void)
|
|||
LOG_I("TASK", "stop ScanThread");
|
||||
rt_thread_detach(&TaskScanThread);
|
||||
}
|
||||
if((TaskUIThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
||||
LOG_I("TASK", "stop UIThread");
|
||||
rt_thread_detach(&TaskUIThread);
|
||||
}
|
||||
/* 软关机 = 系统关闭:停触摸/扫描/BT/伴奏。
|
||||
Main/UI 保留:Type-C 时要画 Charging Image,长按开机要收 POWER_ON。 */
|
||||
if((TaskTouchThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
||||
LOG_I("TASK", "stop TouchThread");
|
||||
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) {
|
||||
LOG_I("TASK", "stop BTHandleThread");
|
||||
rt_thread_detach(&TaskBTHandleThread);
|
||||
|
|
@ -354,6 +379,10 @@ void StopFullTask(void)
|
|||
LOG_I("TASK", "stop BTRecvThread");
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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())
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1017 B |
|
Before Width: | Height: | Size: 902 B After Width: | Height: | Size: 845 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 1.1 KiB |
|
|
@ -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,6 +17,8 @@ from pathlib import Path
|
|||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REPO = ROOT.parent.parent
|
||||
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"
|
||||
|
||||
|
|
|
|||
|
|
@ -61,8 +61,14 @@ def rgba_to_rgb565(im):
|
|||
|
||||
|
||||
def rgba_to_rgb565_opaque(im):
|
||||
"""Full-screen boot/charge: keep dark background pixels (no near-black punch-through)."""
|
||||
im = im.convert("RGB")
|
||||
"""Full-screen boot/charge/flash: keep dark bg; flatten RGBA onto black (not white).
|
||||
|
||||
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()
|
||||
w, h = im.size
|
||||
out = bytearray(w * h * 2)
|
||||
|
|
@ -131,14 +137,14 @@ INTERNAL = [
|
|||
("gImage_UI0902_Volume_20x15", "02_顶部图标/音量图标-10.png"),
|
||||
("gImage_UI0902_Bluetooth_11x17", "02_顶部图标/蓝牙图标-10.png"),
|
||||
# BatteryFill generated by make_battery_fill() — not from mockup PNG
|
||||
("gImage_UI0902_TabSetting_Sel_36x28", "03_底部图标/底部图标-10.png"),
|
||||
("gImage_UI0902_TabSetting_Not_35x28", "03_底部图标/底部图标-11.png"),
|
||||
("gImage_UI0902_TabMixer_Sel_35x27", "03_底部图标/底部图标-12.png"),
|
||||
("gImage_UI0902_TabMixer_Not_18x28", "03_底部图标/底部图标-13.png"),
|
||||
("gImage_UI0902_TabMode_Sel_36x28", "03_底部图标/底部图标-14.png"),
|
||||
("gImage_UI0902_TabMode_Not_35x29", "03_底部图标/底部图标-15.png"),
|
||||
("gImage_UI0902_TabBack_Sel_35x27", "03_底部图标/底部图标-16.png"),
|
||||
("gImage_UI0902_TabBack_Not_18x28", "03_底部图标/底部图标-17.png"),
|
||||
("gImage_UI0902_TabSetting_Sel_40x32", "03_底部图标/底部图标-10.png"), # 万能白
|
||||
("gImage_UI0902_TabSetting_Not_40x32", "03_底部图标/底部图标-11.png"), # 普通白
|
||||
("gImage_UI0902_TabMixer_Sel_40x32", "03_底部图标/底部图标-12.png"), # 专业白
|
||||
("gImage_UI0902_TabMixer_Not_22x32", "03_底部图标/底部图标-13.png"), # 设置白
|
||||
("gImage_UI0902_TabMode_Sel_40x32", "03_底部图标/底部图标-14.png"), # 万能蓝
|
||||
("gImage_UI0902_TabMode_Not_40x32", "03_底部图标/底部图标-15.png"), # 普通蓝
|
||||
("gImage_UI0902_TabBack_Sel_40x32", "03_底部图标/底部图标-16.png"), # 专业蓝
|
||||
("gImage_UI0902_TabBack_Not_22x32", "03_底部图标/底部图标-17.png"), # 设置蓝
|
||||
]
|
||||
|
||||
# ---------------- 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
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Pack external W25Q128 tone/logo image for K1 (0903).
|
||||
"""Pack external W25Q128 tone/logo image for K1 (0914).
|
||||
|
||||
Layout (absolute W25Q128 offsets):
|
||||
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
|
||||
0x0009D07D 2.bin 海阔天空
|
||||
0x000A8607 3.bin 万能模式
|
||||
0x0009D07D 2.bin 本地曲目(变长,<=41KB)
|
||||
after 2.bin 3.bin 万能模式(随 2.bin 长度后移;写入 ExtFlash_Tone_Addr.h)
|
||||
|
||||
Outputs:
|
||||
tools/out/extflash_tone_0903.bin
|
||||
tools/out/extflash_tone_0914.bin / .res
|
||||
tools/out/extflash_ALL_tone0914_ui0902.res
|
||||
project/inc/ExtFlash_Tone_Addr.h
|
||||
Doc/音色文件/0903/FLASH_MAP.txt
|
||||
Doc/音色文件/0914/FLASH_MAP.txt
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
|
@ -24,11 +25,28 @@ from pathlib import Path
|
|||
ROOT = Path(__file__).resolve().parents[1] # firmware project root
|
||||
REPO = ROOT.parent.parent # 一诺国际吉他
|
||||
OUT_DIR = ROOT / "tools" / "out"
|
||||
OUT_BIN = OUT_DIR / "extflash_tone_0903.bin"
|
||||
TONE_TAG = "0914"
|
||||
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_MAP = REPO / "Doc" / "音色文件" / "0903" / "FLASH_MAP.txt"
|
||||
OUT_MAP = REPO / "Doc" / "音色文件" / TONE_TAG / "FLASH_MAP.txt"
|
||||
UI0902_BIN = OUT_DIR / "ui0902_res.bin"
|
||||
|
||||
TONE_DIR = REPO / "Doc" / "音色文件" / "0903"
|
||||
TONE_DIR = REPO / "Doc" / "音色文件" / TONE_TAG
|
||||
|
||||
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:
|
||||
|
|
@ -38,29 +56,69 @@ def find_ziliao() -> Path:
|
|||
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:
|
||||
if not TONE_DIR.is_dir():
|
||||
raise SystemExit(f"missing tone dir: {TONE_DIR}")
|
||||
|
||||
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 = [
|
||||
("LOGO", ziliao / "logo.bin", 0x00000000, "legacy pad; boot uses UI0902_BOOT_LOGO"),
|
||||
("CHARGING", ziliao / "Charg.bin", 0x0000CB70, "legacy pad; UI uses UI0902_CHARGE_SCREEN"),
|
||||
("BIN1_RHYTHM", TONE_DIR / "1.bin", 0x0001B8F0, "普通/专业 31 条节奏"),
|
||||
("BIN2_SONG_HAITIAN", TONE_DIR / "2.bin", None, "本地曲目 海阔天空"),
|
||||
("BIN3_UNIVERSAL", TONE_DIR / "3.bin", None, "万能模式"),
|
||||
("LOGO", ziliao / "logo.bin", OFF_LOGO, "legacy pad; boot uses UI0902_BOOT_LOGO"),
|
||||
("CHARGING", ziliao / "Charg.bin", OFF_CHARGING, "legacy pad; UI uses UI0902_CHARGE_SCREEN"),
|
||||
("BIN1_RHYTHM", bin1_path, OFF_BIN1, "普通/专业 31 条节奏"),
|
||||
("BIN2_SONG_HAITIAN", bin2_path, OFF_BIN2, "本地曲目"),
|
||||
("BIN3_UNIVERSAL", bin3_path, off_bin3, "万能模式(紧跟 2.bin 之后)"),
|
||||
]
|
||||
|
||||
blobs = []
|
||||
blobs: list[bytes] = []
|
||||
cursor = 0
|
||||
rows = []
|
||||
rows: list[tuple[str, int, int, str]] = []
|
||||
for name, path, force_off, note in parts:
|
||||
data = path.read_bytes()
|
||||
if force_off is not None:
|
||||
if cursor > force_off:
|
||||
raise SystemExit(f"{name}: cursor 0x{cursor:X} past forced 0x{force_off:X}")
|
||||
raise SystemExit(
|
||||
f"{name}: cursor 0x{cursor:X} past forced 0x{force_off:X} "
|
||||
f"(previous blob too large)"
|
||||
)
|
||||
if cursor < force_off:
|
||||
pad = force_off - cursor
|
||||
blobs.append(b"\xFF" * pad)
|
||||
rows.append(("(pad)", cursor, pad, "gap fill 0xFF"))
|
||||
cursor = force_off
|
||||
rows.append((f"(pad)", force_off - pad, pad, "gap fill 0xFF"))
|
||||
off = cursor
|
||||
blobs.append(data)
|
||||
cursor += len(data)
|
||||
|
|
@ -69,9 +127,33 @@ def main() -> None:
|
|||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
packed = b"".join(blobs)
|
||||
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("(")}
|
||||
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
|
||||
#define __EXTFLASH_TONE_ADDR_H
|
||||
|
|
@ -79,10 +161,10 @@ def main() -> None:
|
|||
/* Auto-generated by tools/pack_extflash_tone_0903.py — do not hand-edit. */
|
||||
/* 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. */
|
||||
#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_MINOR 9
|
||||
#define EXTFLASH_TONE_RES_VER_PATCH 3
|
||||
#define EXTFLASH_TONE_RES_VER_PATCH 14
|
||||
|
||||
#define EXTFLASH_LOGO_ADDR 0x{by_name['LOGO'][1]:08X}UL
|
||||
#define EXTFLASH_LOGO_SIZE {by_name['LOGO'][2]}UL
|
||||
|
|
@ -92,13 +174,13 @@ def main() -> None:
|
|||
#define EXTFLASH_CHARGING_W 160
|
||||
#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_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_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
|
||||
|
||||
/* Convenience aliases used by UI ADDRESS */
|
||||
|
|
@ -107,7 +189,7 @@ def main() -> None:
|
|||
#define FLASH_ADDR_SONG_HAITIAN EXTFLASH_BIN2_SONG_HAITIAN_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 EXTFLASH_TONE_PACK_END 0x{cursor:08X}UL
|
||||
|
|
@ -119,9 +201,13 @@ def main() -> None:
|
|||
OUT_HDR.write_text(hdr, encoding="utf-8", newline="\n")
|
||||
|
||||
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"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",
|
||||
"-" * 72,
|
||||
|
|
@ -131,32 +217,62 @@ def main() -> None:
|
|||
map_lines += [
|
||||
"",
|
||||
"Firmware ADDRESS mapping:",
|
||||
" 普通/专业 -> FLASH_ADDR_MODE_NORMAL/EXPERT (1.bin)",
|
||||
" 海阔天空 -> FLASH_ADDR_SONG_HAITIAN (2.bin)",
|
||||
" 万能 -> FLASH_ADDR_MODE_UNIVERSAL (3.bin)",
|
||||
" AutoBand -> FLASH_ADDR_AUTOBAND_LEGACY 0x9EB5F (unchanged; overlaps 2.bin region — do not enable until remapped)",
|
||||
" 普通/专业 -> FLASH_ADDR_MODE_NORMAL/EXPERT (1.bin @ 0x1B8F0)",
|
||||
" 本地曲目 -> FLASH_ADDR_SONG_HAITIAN (2.bin @ 0x9D07D, max 41KB)",
|
||||
f" 万能 -> FLASH_ADDR_MODE_UNIVERSAL (3.bin @ 0x{off_bin3:X})",
|
||||
" 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:",
|
||||
" 1. Pack the new preset into 2.bin",
|
||||
" 2. Append a row to local_songs.csv (index,code,name)",
|
||||
" 3. python tools/pack_extflash_tone_0903.py (also regenerates LocalSongNames.h)",
|
||||
" 4. Rebuild firmware and flash MCU + ExtFlash",
|
||||
"Rebuild after pack:",
|
||||
" 1. python tools/pack_extflash_tone_0903.py",
|
||||
" 2. Rebuild MCU (ExtFlash_Tone_Addr.h updated)",
|
||||
" 3. Flash MCU + ExtFlash ALL.res",
|
||||
]
|
||||
OUT_MAP.parent.mkdir(parents=True, exist_ok=True)
|
||||
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_RES} ({len(packed)} bytes)")
|
||||
print(f"Wrote {OUT_HDR}")
|
||||
print(f"Wrote {OUT_MAP}")
|
||||
for name, off, size, note in rows:
|
||||
print(f" 0x{off:08X} {size:8d} {name} {note}")
|
||||
if cursor > 0x00100000:
|
||||
raise SystemExit("ERROR: pack overflows into UI0902_RES_BASE")
|
||||
print(f"OK: {0x00100000 - cursor} bytes free before UI0902 @ 0x00100000")
|
||||
print(f"OK: {UI0902_RES_BASE - cursor} bytes free before UI0902 @ 0x{UI0902_RES_BASE:X}")
|
||||
print("OK: LOGO/CHARG/BIN1/BIN2/BIN3 present and match source files")
|
||||
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"
|
||||
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)
|
||||
if r.returncode != 0:
|
||||
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 -*-
|
||||
"""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:
|
||||
0x00000000 tone pack (logo pad + Charg + 1/2/3.bin) -> *.tone.res / combined
|
||||
0x00100000 UI0902 bitmaps (mode rows, boot logo, ...) -> *.ui0902.res / combined
|
||||
|
||||
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
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
|
@ -19,15 +21,27 @@ from pathlib import Path
|
|||
REPO = Path(r"C:\Users\qjyu\Documents\SoundWalker\一诺国际吉他")
|
||||
PROJ = REPO / "Code" / "YNGJ-GT1-M - AT32F403ARCT7"
|
||||
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 = "0914"
|
||||
TONE_BIN = PROJ / "tools" / "out" / f"extflash_tone_{TONE_RES_VER}.bin"
|
||||
UI0902_BIN = PROJ / "tools" / "out" / "ui0902_res.bin"
|
||||
MAP_TXT = REPO / "Doc" / "音色文件" / "0903" / "FLASH_MAP.txt"
|
||||
MAP_TXT = REPO / "Doc" / "音色文件" / TONE_RES_VER / "FLASH_MAP.txt"
|
||||
TONE_HDR = PROJ / "project" / "inc" / "ExtFlash_Tone_Addr.h"
|
||||
IAP_DIR = REPO / "升级" / "MCU主控升级"
|
||||
OUT_ROOT = REPO / "tools" / "out"
|
||||
|
||||
FW_VER = "0.2.6"
|
||||
TONE_RES_VER = "0903"
|
||||
FW_VER = "0.2.12"
|
||||
UI0902_RES_BASE = 0x00100000
|
||||
BOOT_FLASH_ADDR = 0x08000000
|
||||
APP_FLASH_ADDR = 0x08008000
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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" / "音色文件" / TONE_RES_VER
|
||||
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:
|
||||
if not BOOT_BIN.is_file():
|
||||
raise SystemExit(f"missing Boot bin: {BOOT_BIN}")
|
||||
if not EXE_BIN.is_file():
|
||||
raise SystemExit(f"missing MCU bin: {EXE_BIN}")
|
||||
if not TONE_BIN.is_file():
|
||||
tone_bin = TONE_BIN
|
||||
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():
|
||||
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"
|
||||
guide = IAP_DIR / "升级步骤.docx"
|
||||
|
|
@ -82,16 +164,24 @@ def main() -> None:
|
|||
shutil.rmtree(pkg)
|
||||
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()
|
||||
if len(ui_data) < 1024:
|
||||
raise SystemExit(f"UI0902 pack too small: {len(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"
|
||||
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
|
||||
comb_dst = pkg / comb_name
|
||||
|
||||
shutil.copy2(BOOT_BIN, boot_dst)
|
||||
shutil.copy2(EXE_BIN, mcu_dst)
|
||||
comb_dst.write_bytes(combined)
|
||||
shutil.copy2(iap, pkg / "SoundWalkerIAP.exe")
|
||||
|
|
@ -99,10 +189,10 @@ def main() -> None:
|
|||
if MAP_TXT.is_file():
|
||||
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(comb_dst, OUT_ROOT / "extflash_ALL_tone0903_ui0902.res")
|
||||
# Keep build intermediates under tools/out for local rebuilds; not shipped in package.
|
||||
(OUT_ROOT / "extflash_tone_0903.res").write_bytes(tone_data)
|
||||
shutil.copy2(comb_dst, OUT_ROOT / f"extflash_ALL_tone{TONE_RES_VER}_ui0902.res")
|
||||
(OUT_ROOT / f"extflash_tone_{TONE_RES_VER}.res").write_bytes(tone_data)
|
||||
(OUT_ROOT / "extflash_ui0902.res").write_bytes(ui_data)
|
||||
|
||||
readme = pkg / "README.txt"
|
||||
|
|
@ -115,28 +205,36 @@ def main() -> None:
|
|||
f"Git subject {subject}",
|
||||
f"FW version {FW_VER}",
|
||||
"",
|
||||
"==== 整机升级请刷这两项 ====",
|
||||
f" 1) {mcu_name} -> MCU APP",
|
||||
f" 2) {comb_name} -> 外部 Flash 从 0x0 起整包",
|
||||
"==== 整机升级请刷这三项 ====",
|
||||
f" 1) {boot_name} -> Bootloader @ 0x{BOOT_FLASH_ADDR:08X}",
|
||||
f" 2) {mcu_name} -> MCU APP @ 0x{APP_FLASH_ADDR:08X}",
|
||||
f" 3) {comb_name} -> 外部 Flash 从 0x0 起整包",
|
||||
" (= 音色区 + 填充 + UI0902 图片区)",
|
||||
"",
|
||||
"建议步骤:",
|
||||
" - 先刷 Boot(否则 USB 烧录等待画面仍是旧红字「升级模式」)",
|
||||
" - 再刷 APP",
|
||||
" - 先整片擦除外部 Flash,再刷上述 ALL .res @ 0x00000000",
|
||||
" - 勿只刷音色区,否则模式选择页会花屏(UI 图在 0x100000)",
|
||||
"",
|
||||
"外部 Flash 分区:",
|
||||
" 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、充电、烧录模式、设置/调音台等)",
|
||||
" 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" 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 占位。",
|
||||
" - AutoBand 0x9EB5F 本版未重排。",
|
||||
f" - 本包音色来源 Doc/音色文件/{TONE_RES_VER}/;BIN3 随 2.bin 长度后移。",
|
||||
"",
|
||||
"附件: 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
|
||||