Compare commits
32 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
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 |
|
|
@ -303,6 +303,14 @@ void app_adc_sync_volume(uint16_t val)
|
||||||
app_adc_volume_map_level(val);
|
app_adc_volume_map_level(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 取当前主音量(复用扫描任务维护的最近一次有效值)。
|
||||||
|
* 模拟开关同一时刻只选中一路,离开扫描上下文直接重读 ADC 会串到
|
||||||
|
* 其它通道,故统一走 vol_last;未初始化(0xFFFF)时按静音兜底 */
|
||||||
|
uint8_t app_adc_get_volume(void)
|
||||||
|
{
|
||||||
|
return (vol_last > 127) ? 0 : (uint8_t)vol_last;
|
||||||
|
}
|
||||||
|
|
||||||
static void app_adc_volume_process(void)
|
static void app_adc_volume_process(void)
|
||||||
{
|
{
|
||||||
uint16_t val = ADC_ReadChannel(ADC_CHANNEL_1)>>5;
|
uint16_t val = ADC_ReadChannel(ADC_CHANNEL_1)>>5;
|
||||||
|
|
|
||||||
|
|
@ -27,4 +27,6 @@ void app_adc_in1_scan_part(uint8_t channel);
|
||||||
bool app_adc_usb_is_plugged(void);
|
bool app_adc_usb_is_plugged(void);
|
||||||
/* 开机/主动采样后同步 vol_last 与 level,避免扫描任务误触发或跳变 */
|
/* 开机/主动采样后同步 vol_last 与 level,避免扫描任务误触发或跳变 */
|
||||||
void app_adc_sync_volume(uint16_t val);
|
void app_adc_sync_volume(uint16_t val);
|
||||||
|
/* 取当前主音量(0~127),供 MasterVolume_Reapply 等重发场景使用 */
|
||||||
|
uint8_t app_adc_get_volume(void);
|
||||||
#endif
|
#endif
|
||||||
152
APP/app_log.c
|
|
@ -11,7 +11,7 @@ static uint32_t s_overflow;
|
||||||
static uint32_t s_boot_count;
|
static uint32_t s_boot_count;
|
||||||
static char s_ui_page[16] = "boot";
|
static char s_ui_page[16] = "boot";
|
||||||
static char s_debug_level = 'D';
|
static char s_debug_level = 'D';
|
||||||
static char s_cmd_buf[48];
|
static char s_cmd_buf[64];
|
||||||
static uint8_t s_cmd_len;
|
static uint8_t s_cmd_len;
|
||||||
|
|
||||||
/* RTT 二进制烧录:写到 W25Q128(ui0902 或本地曲目) */
|
/* RTT 二进制烧录:写到 W25Q128(ui0902 或本地曲目) */
|
||||||
|
|
@ -136,7 +136,9 @@ void app_log_set_ui_page(const char *name)
|
||||||
|
|
||||||
void app_log_write(char level, const char *cat, const char *fmt, ...)
|
void app_log_write(char level, const char *cat, const char *fmt, ...)
|
||||||
{
|
{
|
||||||
char line[APP_LOG_SLOT_SIZE];
|
/* 静态行缓冲:TaskBTRecv 等小栈线程禁止再开 128B 局部数组 */
|
||||||
|
static char line[APP_LOG_SLOT_SIZE];
|
||||||
|
static rt_mutex_t s_log_mtx = RT_NULL;
|
||||||
va_list ap;
|
va_list ap;
|
||||||
int n;
|
int n;
|
||||||
|
|
||||||
|
|
@ -144,9 +146,21 @@ void app_log_write(char level, const char *cat, const char *fmt, ...)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (s_log_mtx == RT_NULL) {
|
||||||
|
s_log_mtx = rt_mutex_create("applog", RT_IPC_FLAG_PRIO);
|
||||||
|
}
|
||||||
|
if (s_log_mtx != RT_NULL) {
|
||||||
|
if (rt_mutex_take(s_log_mtx, rt_tick_from_millisecond(20)) != RT_EOK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
n = snprintf(line, sizeof(line), "[%05u][%c][%-4s] ",
|
n = snprintf(line, sizeof(line), "[%05u][%c][%-4s] ",
|
||||||
(unsigned)app_log_ms(), level, cat);
|
(unsigned)app_log_ms(), level, cat);
|
||||||
if (n < 0) {
|
if (n < 0) {
|
||||||
|
if (s_log_mtx != RT_NULL) {
|
||||||
|
rt_mutex_release(s_log_mtx);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((size_t)n >= sizeof(line)) {
|
if ((size_t)n >= sizeof(line)) {
|
||||||
|
|
@ -160,6 +174,10 @@ void app_log_write(char level, const char *cat, const char *fmt, ...)
|
||||||
app_log_slot_store(line);
|
app_log_slot_store(line);
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "\n");
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "\n");
|
||||||
|
|
||||||
|
if (s_log_mtx != RT_NULL) {
|
||||||
|
rt_mutex_release(s_log_mtx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void app_log_dump_ram(void)
|
static void app_log_dump_ram(void)
|
||||||
|
|
@ -280,6 +298,12 @@ uint8_t app_log_try_command(const char *cmd)
|
||||||
"FLASH_HAITIAN_GO", "FLASH_HAITIAN_OK\n");
|
"FLASH_HAITIAN_GO", "FLASH_HAITIAN_OK\n");
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
|
if (strncmp(cmd, "flash bin3 ", 11) == 0) {
|
||||||
|
uint32_t size = (uint32_t)strtoul(cmd + 11, NULL, 10);
|
||||||
|
app_log_flash_start_at(EXTFLASH_BIN3_UNIVERSAL_ADDR, size,
|
||||||
|
"FLASH_BIN3_GO", "FLASH_BIN3_OK\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
if (strncmp(cmd, "tone status", 11) == 0) {
|
if (strncmp(cmd, "tone status", 11) == 0) {
|
||||||
uint8_t hdr[16];
|
uint8_t hdr[16];
|
||||||
uint8_t name[40];
|
uint8_t name[40];
|
||||||
|
|
@ -320,6 +344,16 @@ uint8_t app_log_try_command(const char *cmd)
|
||||||
(unsigned long)(cur_hdr[12] | (cur_hdr[13] << 8) |
|
(unsigned long)(cur_hdr[12] | (cur_hdr[13] << 8) |
|
||||||
(cur_hdr[14] << 16) | (cur_hdr[15] << 24)));
|
(cur_hdr[14] << 16) | (cur_hdr[15] << 24)));
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
{
|
||||||
|
uint8_t b3[16];
|
||||||
|
W25Q128_Read(b3, EXTFLASH_BIN3_UNIVERSAL_ADDR, 16);
|
||||||
|
snprintf(line, sizeof(line),
|
||||||
|
"TONE @BIN3 magic=%02X%02X%02X%02X cnt=%lu\n",
|
||||||
|
b3[0], b3[1], b3[2], b3[3],
|
||||||
|
(unsigned long)(b3[12] | (b3[13] << 8) |
|
||||||
|
(b3[14] << 16) | (b3[15] << 24)));
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
}
|
||||||
snprintf(line, sizeof(line),
|
snprintf(line, sizeof(line),
|
||||||
"TONE @BIN2 magic=%02X%02X%02X%02X max=%lu cnt=%lu name=%.16s\n",
|
"TONE @BIN2 magic=%02X%02X%02X%02X max=%lu cnt=%lu name=%.16s\n",
|
||||||
hdr[0], hdr[1], hdr[2], hdr[3],
|
hdr[0], hdr[1], hdr[2], hdr[3],
|
||||||
|
|
@ -364,16 +398,126 @@ uint8_t app_log_try_command(const char *cmd)
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
|
/* tone bin1 [idx] → 1.bin 节奏(专业+节奏类型) */
|
||||||
|
if (strncmp(cmd, "tone bin1", 9) == 0) {
|
||||||
|
char line[160];
|
||||||
|
int ret;
|
||||||
|
unsigned idx = 0;
|
||||||
|
int count;
|
||||||
|
if (cmd[9] == ' ' && cmd[10] != '\0')
|
||||||
|
idx = (unsigned)strtoul(cmd + 10, NULL, 10);
|
||||||
|
mGuiData[GUI_TAB_INDEX].Current = 1; /* 专业 */
|
||||||
|
mGuiData[GUI_AUTOBAND_SW].Current = 0; /* 节奏类型→1.bin */
|
||||||
|
ParamGuiData[SONG_MODE_PARAM].Current = (uint8_t)idx;
|
||||||
|
UI_ApplyToneAddress();
|
||||||
|
AutoBandTop1_Stop();
|
||||||
|
StartFlag = 0;
|
||||||
|
count = AutoBandTop1_GetPresetItemCount();
|
||||||
|
if (count > 0 && (int)idx >= count)
|
||||||
|
idx = (unsigned)(count - 1);
|
||||||
|
ParamGuiData[SONG_MODE_PARAM].Current = (uint8_t)idx;
|
||||||
|
ret = AutoBandTop1_LoadPresetItemFromFlash((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 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 = AutoBandTop1_LoadPresetItemFromFlash((int)idx);
|
||||||
|
snprintf(line, sizeof(line),
|
||||||
|
"TONE_BIN3 ret=%d idx=%u addr=0x%08lX map=BIN3@0x%08lX name=%s count=%d %s\n",
|
||||||
|
ret, idx, (unsigned long)ADDRESS,
|
||||||
|
(unsigned long)EXTFLASH_BIN3_UNIVERSAL_ADDR,
|
||||||
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "(null)",
|
||||||
|
AutoBandTop1_GetPresetItemCount(),
|
||||||
|
(ADDRESS == EXTFLASH_BIN3_UNIVERSAL_ADDR) ? "OK" : "MISMATCH");
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
/* tone start:在当前已加载音色上拨片起奏(不改 TAB/库) */
|
||||||
|
if (strncmp(cmd, "tone start", 10) == 0) {
|
||||||
|
StartFlag = 0;
|
||||||
|
if (!(cmd[10] == ' ' && (cmd[11] == 'k' || cmd[11] == 'K')))
|
||||||
|
KEY_ID_1629 = 0;
|
||||||
|
Pick_Handle();
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_START_DONE\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
if (strncmp(cmd, "tone pick", 9) == 0) {
|
if (strncmp(cmd, "tone pick", 9) == 0) {
|
||||||
/* 模拟专业+本地曲目拨片(无和弦板) */
|
/* tone pick uni → 万能;默认仍测专业+本地曲目 */
|
||||||
|
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)AutoBandTop1_LoadPresetItemFromFlash(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;
|
||||||
|
}
|
||||||
|
/* 模拟专业+本地曲目拨片;chord pick keep → 保留已注入的 KEY_ID */
|
||||||
mGuiData[GUI_TAB_INDEX].Current = 1;
|
mGuiData[GUI_TAB_INDEX].Current = 1;
|
||||||
mGuiData[GUI_AUTOBAND_SW].Current = 1;
|
mGuiData[GUI_AUTOBAND_SW].Current = 1;
|
||||||
KEY_ID_1629 = 0; /* 走默认和弦兜底 */
|
if (!(cmd[9] == ' ' && (cmd[10] == 'k' || cmd[10] == 'K'))) {
|
||||||
|
KEY_ID_1629 = 0; /* 默认走一级和弦兜底 */
|
||||||
|
}
|
||||||
StartFlag = 0;
|
StartFlag = 0;
|
||||||
Pick_Handle();
|
Pick_Handle();
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_PICK_DONE\n");
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_PICK_DONE\n");
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
|
/* 测试注入:chord key N(0释放 / 1~21和弦 / 22拍速 / 23停止) */
|
||||||
|
if (strncmp(cmd, "chord key ", 10) == 0 && cmd[10] != '\0') {
|
||||||
|
unsigned key = (unsigned)strtoul(cmd + 10, NULL, 10);
|
||||||
|
char line[48];
|
||||||
|
if (key > 23U) {
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "CHORD_KEY_BAD\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
app_tm1629_inject_key((uint8_t)key);
|
||||||
|
snprintf(line, sizeof(line), "CHORD_KEY_OK key=%u\n", key);
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
/* 测试注入:chord xpose N(0~11,C=0) */
|
||||||
|
if (strncmp(cmd, "chord xpose ", 12) == 0 && cmd[12] != '\0') {
|
||||||
|
unsigned xp = (unsigned)strtoul(cmd + 12, NULL, 10);
|
||||||
|
char line[48];
|
||||||
|
if (xp > 11U) {
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "CHORD_XPOSE_BAD\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
mGuiData[GUI_TRANSPOSE].Current = (uint8_t)xp;
|
||||||
|
snprintf(line, sizeof(line), "CHORD_XPOSE_OK xp=%u\n", xp);
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
LOG_I("REG", "xpose set %u", xp);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
if (strncmp(cmd, "flash erase chip", 16) == 0) {
|
if (strncmp(cmd, "flash erase chip", 16) == 0) {
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "FLASH_ERASE_BEGIN\n");
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "FLASH_ERASE_BEGIN\n");
|
||||||
LOG_I("FLASH", "W25Q128 chip erase start");
|
LOG_I("FLASH", "W25Q128 chip erase start");
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,13 @@ uint8_t app_tm1629_Scan_Key(void)
|
||||||
return KEY_NONE;
|
return KEY_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void app_tm1629_inject_key(uint8_t key)
|
||||||
|
{
|
||||||
|
key_last = key;
|
||||||
|
LOG_I("KEY", "inject key=%u", (unsigned)key);
|
||||||
|
TM1629_Handle(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void app_tm1629_led_set(LedNum_TypeDef led, LedColor_TypeDef color, uint8_t on)
|
static void app_tm1629_led_set(LedNum_TypeDef led, LedColor_TypeDef color, uint8_t on)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
void app_tm1629_init(void);
|
void app_tm1629_init(void);
|
||||||
uint8_t app_tm1629_Scan_Key(void);
|
uint8_t app_tm1629_Scan_Key(void);
|
||||||
|
void app_tm1629_inject_key(uint8_t key); /* RTT/测试:绕过扫描直接注入 */
|
||||||
void app_tm1629_set_chord_led(uint8_t key_idx, LedColor_TypeDef color);
|
void app_tm1629_set_chord_led(uint8_t key_idx, LedColor_TypeDef color);
|
||||||
void app_tm1629_set_led(LedNum_TypeDef led, LedColor_TypeDef color);
|
void app_tm1629_set_led(LedNum_TypeDef led, LedColor_TypeDef color);
|
||||||
void app_tm1629_all_off(void);
|
void app_tm1629_all_off(void);
|
||||||
|
|
|
||||||
108
Global/Global.c
|
|
@ -89,11 +89,11 @@ const Touch_AreaTypeDef Touch_Areas[] =
|
||||||
{ 126, 172, UI0902_SECTION_Y, UI0902_SECTION_Y + UI0902_SECTION_BTN_H - 1, 0, 2, GUI_PLAY_SECTION, NULL },
|
{ 126, 172, UI0902_SECTION_Y, UI0902_SECTION_Y + UI0902_SECTION_BTN_H - 1, 0, 2, GUI_PLAY_SECTION, NULL },
|
||||||
{ 183, 229, UI0902_SECTION_Y, UI0902_SECTION_Y + UI0902_SECTION_BTN_H - 1, 0, 3, GUI_PLAY_SECTION, NULL },
|
{ 183, 229, UI0902_SECTION_Y, UI0902_SECTION_Y + UI0902_SECTION_BTN_H - 1, 0, 3, GUI_PLAY_SECTION, NULL },
|
||||||
|
|
||||||
/* 底栏四键:万能 | 普通 | 专业 | 设置(触区中线对齐 cx 40/100/160/210) */
|
/* 底栏四键:万能 | 普通 | 专业 | 设置(60×55,y265–319) */
|
||||||
{ 0, 69, 280, 319, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(0), UI0902_NAV_X1(0), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
|
||||||
{ 70, 129, 280, 319, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(1), UI0902_NAV_X1(1), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
|
||||||
{ 130, 184, 280, 319, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(2), UI0902_NAV_X1(2), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
|
||||||
{ 185, 239, 280, 319, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(3), UI0902_NAV_X1(3), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
|
||||||
};
|
};
|
||||||
|
|
||||||
/* 推子 0~10 共 11 档;档10 取 AW816 上限附近 */
|
/* 推子 0~10 共 11 档;档10 取 AW816 上限附近 */
|
||||||
|
|
@ -101,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));
|
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,8}; /* major.minor.patch — 0.2.8: BT recv stack/log harden; pitch/chord SysEx bounds */
|
||||||
|
|
||||||
uint8_t Led = 8;
|
uint8_t Led = 8;
|
||||||
bool PressFlag = 0;
|
bool PressFlag = 0;
|
||||||
|
|
@ -184,6 +184,7 @@ void System_PowerOn(void)
|
||||||
BSP_MainPowerEnable(1);
|
BSP_MainPowerEnable(1);
|
||||||
BSP_DreamCorePowerEnable(1);
|
BSP_DreamCorePowerEnable(1);
|
||||||
BSP_HT7178PowerEnable(1);
|
BSP_HT7178PowerEnable(1);
|
||||||
|
/* 蓝牙按 NVM/系统设置开关恢复,勿强制常开 */
|
||||||
BSP_BlueToothPowerEnable((uint8_t)(mGuiData[GUI_BL_SW].Current ? 1 : 0));
|
BSP_BlueToothPowerEnable((uint8_t)(mGuiData[GUI_BL_SW].Current ? 1 : 0));
|
||||||
powon = true; /* 更新开机标志 */
|
powon = true; /* 更新开机标志 */
|
||||||
|
|
||||||
|
|
@ -196,7 +197,9 @@ void System_PowerOn(void)
|
||||||
app_tm1617_init();
|
app_tm1617_init();
|
||||||
XPT2046_Init();
|
XPT2046_Init();
|
||||||
App_Auto_Init();
|
App_Auto_Init();
|
||||||
LOG_I("PWR", "periph ready");
|
/* App 协议走 UART4:须在开机后即解析,不能等到触摸选模式才 StartTask */
|
||||||
|
StartTask();
|
||||||
|
LOG_I("PWR", "periph ready + BT UART recv started");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void PowerOn(void)
|
static void PowerOn(void)
|
||||||
|
|
@ -226,20 +229,23 @@ static void PowerOff(void)
|
||||||
|
|
||||||
LCD_FillByColor(0, 0, 240, 320, BLACK);
|
LCD_FillByColor(0, 0, 240, 320, BLACK);
|
||||||
LCD_BLK_Clr();
|
LCD_BLK_Clr();
|
||||||
|
LCD_WR_REG(0x28); /* Display OFF */
|
||||||
|
|
||||||
app_tm1629_all_off();
|
app_tm1629_all_off();
|
||||||
|
|
||||||
app_tm1617_off();
|
app_tm1617_off();
|
||||||
/* 关闭电源硬件;MCU 保持运行以便长按开机 / 关机充电画面 */
|
/* 关掉主电源/音源/功放/蓝牙;MCU 仍跑以便长按开机与 Type-C 检测 */
|
||||||
BSP_MainPowerEnable(0);
|
BSP_MainPowerEnable(0);
|
||||||
BSP_DreamCorePowerEnable(0);
|
BSP_DreamCorePowerEnable(0);
|
||||||
BSP_HT7178PowerEnable(0);
|
BSP_HT7178PowerEnable(0);
|
||||||
|
/* BT 最后关:给已排队的 UART4 ACK(如 05 00)留出模组转发窗口 */
|
||||||
|
rt_thread_mdelay(30);
|
||||||
BSP_BlueToothPowerEnable(0);
|
BSP_BlueToothPowerEnable(0);
|
||||||
CurrUIProcress = IdleProcess;
|
CurrUIProcress = IdleProcess;
|
||||||
MainTask_Sendmsg(MSG_ID_POWER_OFF, 0, 0, 0);
|
MainTask_Sendmsg(MSG_ID_POWER_OFF, 0, 0, 0);
|
||||||
LOG_I("PWR", "power_off done (soft-off, no reset)");
|
LOG_I("PWR", "power_off done (soft-off)");
|
||||||
|
|
||||||
/* 关机时若已插着 Type-C,立刻进充电画面 */
|
/* Type-C 有供电:维持现有逻辑,显示 Charging Image(POWOFF_CHARG) */
|
||||||
{
|
{
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
for (i = 0; i < 4; i++)
|
for (i = 0; i < 4; i++)
|
||||||
|
|
@ -253,12 +259,29 @@ static void PowerOff(void)
|
||||||
{
|
{
|
||||||
usb_charging_state = 1;
|
usb_charging_state = 1;
|
||||||
usb_charging_laststate = 1;
|
usb_charging_laststate = 1;
|
||||||
LOG_I("PWR", "soft-off with USB → charge UI");
|
LOG_I("PWR", "soft-off with Type-C → charge UI");
|
||||||
MainTask_Sendmsg(MSG_ID_POWOFF_CHARG, percentage, 1, 0);
|
MainTask_Sendmsg(MSG_ID_POWOFF_CHARG, percentage, 1, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* App remote power-off request (BLE cmd 05 00): executed in main-loop context,
|
||||||
|
so StopFullTask() never detaches the calling BT thread itself. */
|
||||||
|
static volatile uint8_t s_sys_poweroff_req = 0;
|
||||||
|
void System_RequestPowerOff(void)
|
||||||
|
{
|
||||||
|
s_sys_poweroff_req = 1;
|
||||||
|
}
|
||||||
|
void System_PowerOff_Poll(void)
|
||||||
|
{
|
||||||
|
if (s_sys_poweroff_req)
|
||||||
|
{
|
||||||
|
s_sys_poweroff_req = 0;
|
||||||
|
if (powon)
|
||||||
|
PowerOff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Power_Key_Scan()
|
void Power_Key_Scan()
|
||||||
{
|
{
|
||||||
flag_status status = BSP_GetPowerKey();
|
flag_status status = BSP_GetPowerKey();
|
||||||
|
|
@ -437,6 +460,16 @@ void Send_volume(uint8_t vol)
|
||||||
SendMidiDataToDreamDSP(mMasterSysExVolume,12);
|
SendMidiDataToDreamDSP(mMasterSysExVolume,12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 重发当前主音量:开机补发 / 进模式主页 / 开始播放前调用。
|
||||||
|
* Dream 上电早期可能丢 SysEx,丢失后旋钮不动就不再重发,
|
||||||
|
* 会出现"图标低音量、实际音量大";在关键时机重发可自愈 */
|
||||||
|
void MasterVolume_Reapply(void)
|
||||||
|
{
|
||||||
|
uint8_t vol = app_adc_get_volume();
|
||||||
|
app_adc_sync_volume(vol); /* 兜底刷新 level,保证图标与下发值一致 */
|
||||||
|
Send_volume(vol);
|
||||||
|
}
|
||||||
|
|
||||||
void Accomp_UpdatePlayingChord(void)
|
void Accomp_UpdatePlayingChord(void)
|
||||||
{
|
{
|
||||||
uint8_t midi_note_buff[4] = {0,};
|
uint8_t midi_note_buff[4] = {0,};
|
||||||
|
|
@ -471,6 +504,8 @@ void ADC_IN1_KEY_Handle(uint8_t key,bool on)
|
||||||
case 1: AutoBandTop1_Postamble(0); break;
|
case 1: AutoBandTop1_Postamble(0); break;
|
||||||
case 2: AutoBandTop1_Postamble(1); break;
|
case 2: AutoBandTop1_Postamble(1); break;
|
||||||
}
|
}
|
||||||
|
/* 前奏/尾奏起奏前补发主音量,保证与图标一致 */
|
||||||
|
MasterVolume_Reapply();
|
||||||
//AutoBandTop1_Start();
|
//AutoBandTop1_Start();
|
||||||
AutoBandTop1_SyncStart();
|
AutoBandTop1_SyncStart();
|
||||||
wk_delay_ms(50);
|
wk_delay_ms(50);
|
||||||
|
|
@ -738,6 +773,10 @@ void Pick_Handle()
|
||||||
note_cnt = GetChordNotesByType(chord_id, chord_type_index_map[chord_id].type, midi_note_buff);
|
note_cnt = GetChordNotesByType(chord_id, chord_type_index_map[chord_id].type, midi_note_buff);
|
||||||
Return_Light_buff(KEY_ID_1629);
|
Return_Light_buff(KEY_ID_1629);
|
||||||
|
|
||||||
|
/* 首次起奏前补发主音量:防开机 SysEx 丢失后"图标低、实际大" */
|
||||||
|
if (StartFlag == 0)
|
||||||
|
MasterVolume_Reapply();
|
||||||
|
|
||||||
switch(mGuiData[GUI_TAB_INDEX].Current)
|
switch(mGuiData[GUI_TAB_INDEX].Current)
|
||||||
{
|
{
|
||||||
case 3:
|
case 3:
|
||||||
|
|
@ -760,15 +799,10 @@ void Pick_Handle()
|
||||||
AutoBandTop1_SetLoopMode(0,1);
|
AutoBandTop1_SetLoopMode(0,1);
|
||||||
break;
|
break;
|
||||||
case 0:
|
case 0:
|
||||||
if(PressFlag)
|
/* 设置页内沿用万能:与主路径一致,始终 loop */
|
||||||
{
|
AutoBandTop1_SetPiecePlayMode(1, 0, 480);
|
||||||
AutoBandTop1_SetLoopMode(0,1);
|
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
AutoBandTop1_SetLoopMode(0, 1);
|
||||||
}else
|
|
||||||
{
|
|
||||||
AutoBandTop1_SetPiecePlayMode(1,0,480);
|
|
||||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
|
||||||
}
|
|
||||||
StartFlag = 1;
|
StartFlag = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -819,17 +853,14 @@ void Pick_Handle()
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0:
|
case 0:
|
||||||
|
/* 万能:和弦走向用 piece 模式;须 SetLoopMode,否则无循环时常听不到伴奏 */
|
||||||
StartFlag = 1;
|
StartFlag = 1;
|
||||||
if(PressFlag)
|
AutoBandTop1_SetPiecePlayMode(1, 0, 480);
|
||||||
{
|
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||||
AutoBandTop1_SetPiecePlayMode(1,0,480);
|
AutoBandTop1_SetLoopMode(0, 1);
|
||||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
LOG_I("pick", "universal piece name=%s press=%u notes=%u chord=%u",
|
||||||
AutoBandTop1_SetLoopMode(0,1);
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
||||||
}else
|
(unsigned)PressFlag, (unsigned)note_cnt, (unsigned)chord_id);
|
||||||
{
|
|
||||||
AutoBandTop1_SetPiecePlayMode(1,0,480);
|
|
||||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -941,9 +972,8 @@ void BL_Set_led(uint8_t led, uint8_t chord_type)
|
||||||
|
|
||||||
void BT_Pitch_offset_map(uint8_t pos_offset,uint8_t pitch_offset)
|
void BT_Pitch_offset_map(uint8_t pos_offset,uint8_t pitch_offset)
|
||||||
{
|
{
|
||||||
//char Info[10];
|
if (pos_offset < 1u || pos_offset > 21u)
|
||||||
//sprintf (Info, "%d %d", pos_offset,pitch_offset);
|
return;
|
||||||
//LCD_ShowString(2, 156+16, (const uint8_t*)Info, RED, WHITE, 16, 0);
|
|
||||||
chord_type_index_map[pos_offset].PitchOffset = pitch_offset;
|
chord_type_index_map[pos_offset].PitchOffset = pitch_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -952,10 +982,13 @@ void BT_Chord_offset_map(uint8_t pos_offset,uint8_t chord_offset)
|
||||||
//char Info[20];
|
//char Info[20];
|
||||||
STRING_MIDI *MIDI;
|
STRING_MIDI *MIDI;
|
||||||
const STRING_MIDI *ORGAN_MIDI;
|
const STRING_MIDI *ORGAN_MIDI;
|
||||||
|
uint8_t group;
|
||||||
uint8_t group = (pos_offset - 1) / 3; /* 1~3→0, 4~6→1, ..., 19~21→6 */
|
|
||||||
uint8_t chord_APP_Local_offset = 0;
|
uint8_t chord_APP_Local_offset = 0;
|
||||||
|
|
||||||
|
if (pos_offset < 1u || pos_offset > 21u)
|
||||||
|
return;
|
||||||
|
group = (pos_offset - 1) / 3; /* 1~3→0, 4~6→1, ..., 19~21→6 */
|
||||||
|
|
||||||
switch(chord_offset){
|
switch(chord_offset){
|
||||||
case 0x00:
|
case 0x00:
|
||||||
chord_APP_Local_offset = 0;
|
chord_APP_Local_offset = 0;
|
||||||
|
|
@ -1280,9 +1313,10 @@ uint8_t GetChordNotesByType(uint8_t MIDI_Index,uint8_t type,uint8_t *buff)
|
||||||
root = buff[0];
|
root = buff[0];
|
||||||
for (i = 1; i < n; i++)
|
for (i = 1; i < n; i++)
|
||||||
{
|
{
|
||||||
while (buff[i] >= (uint8_t)(root + 12))
|
uint8_t guard;
|
||||||
|
for (guard = 0; guard < 2 && buff[i] >= (uint8_t)(root + 12); guard++)
|
||||||
buff[i] -= 12;
|
buff[i] -= 12;
|
||||||
while (buff[i] < root)
|
for (guard = 0; guard < 2 && buff[i] < root; guard++)
|
||||||
buff[i] += 12;
|
buff[i] += 12;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,8 @@ extern bool powon;
|
||||||
void Power_Key_Scan(void);
|
void Power_Key_Scan(void);
|
||||||
void AutoPowerOff_Scan(void);
|
void AutoPowerOff_Scan(void);
|
||||||
void System_PowerOn(void);
|
void System_PowerOn(void);
|
||||||
|
void System_RequestPowerOff(void);
|
||||||
|
void System_PowerOff_Poll(void);
|
||||||
extern uint8_t Led;
|
extern uint8_t Led;
|
||||||
extern bool PressFlag;
|
extern bool PressFlag;
|
||||||
|
|
||||||
|
|
@ -211,6 +213,7 @@ uint8_t GUI_Item_AjustValue(GUI_SWITCH * Group, int8_t Dir);
|
||||||
void GUI_Item_AjustLoopValue(GUI_SWITCH * Group, int8_t Dir);
|
void GUI_Item_AjustLoopValue(GUI_SWITCH * Group, int8_t Dir);
|
||||||
|
|
||||||
void Send_volume(uint8_t vol);
|
void Send_volume(uint8_t vol);
|
||||||
|
void MasterVolume_Reapply(void);
|
||||||
|
|
||||||
void ADC_IN1_KEY_Handle(uint8_t key,bool on);
|
void ADC_IN1_KEY_Handle(uint8_t key,bool on);
|
||||||
void TM1617_Handle(uint8_t key);
|
void TM1617_Handle(uint8_t key);
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,318 @@ static uint8_t pStoreBuffer[PRESET_BUFFER_SIZE];
|
||||||
//static uint8_t* pStoreBuffer;
|
//static uint8_t* pStoreBuffer;
|
||||||
#define DEFAULT_TEMPO 100
|
#define DEFAULT_TEMPO 100
|
||||||
|
|
||||||
|
/* ======== Bass/和弦分通道八度(无 AutoBand 源码时的输出侧映射) ========
|
||||||
|
* 代码通道(0 起,与 MIDI status 低 4 位一致):8=bass,9=鼓;其余=和弦。
|
||||||
|
* 日志打印同号(ch8=bass)。I/II(键1~6)原样;III+:bass -12 地板 E1;
|
||||||
|
* 和弦 -12 夹在 E2~#G4,超上限就近和弦内音。移调≥#F(6) 时再 -12。
|
||||||
|
* 对照 Doc/吉他和弦表最终版.xlsx */
|
||||||
|
#define CHORD_REG_FIX_EN 1
|
||||||
|
#define BASS_CH 8 /* 日志 ch8 */
|
||||||
|
#define DRUM_CH 9 /* 日志 ch9 */
|
||||||
|
#define MIDI_E1 28
|
||||||
|
#define MIDI_E2 40
|
||||||
|
#define MIDI_GSHARP4 68
|
||||||
|
#define XPOSE_FS 6 /* C=0 时 #F=6 */
|
||||||
|
|
||||||
|
/* 映射策略指纹:变化时 CC123 清旧音,防挂音(不建大音符表) */
|
||||||
|
static uint8_t s_reg_fp = 0xFFu;
|
||||||
|
/* 每个指纹周期内 NoteOn 映射日志限额(和弦与 bass 分开,避免和弦占满看不到 bass) */
|
||||||
|
static uint8_t s_pitch_log_left = 0;
|
||||||
|
static uint8_t s_bass_log_left = 0;
|
||||||
|
static uint16_t s_ch_ever_mask = 0; /* 会话内各通道是否出现过 NoteOn */
|
||||||
|
#define PITCH_LOG_PER_FP 24
|
||||||
|
#define BASS_LOG_PER_FP 16
|
||||||
|
|
||||||
|
static int App_Auto_ClampMidi(int n)
|
||||||
|
{
|
||||||
|
if (n < 0) return 0;
|
||||||
|
if (n > 127) return 127;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_ChordDegree(void)
|
||||||
|
{
|
||||||
|
if (KEY_ID_1629 < 1 || KEY_ID_1629 > 21)
|
||||||
|
return 0;
|
||||||
|
return (int)((KEY_ID_1629 - 1) / 3) + 1; /* 1=I .. 7=VII */
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_NeedRegisterFix(void)
|
||||||
|
{
|
||||||
|
/* 键 1~6 = I/II:不动;7~21 = III~VII */
|
||||||
|
return (KEY_ID_1629 >= 7 && KEY_ID_1629 <= 21) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_XposeExtra(void)
|
||||||
|
{
|
||||||
|
return (mGuiData[GUI_TRANSPOSE].Current >= XPOSE_FS) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t App_Auto_RegFingerprint(void)
|
||||||
|
{
|
||||||
|
/* bit0=need_fix, bit1=xpose_extra, bit2.. = 粗粒度级数 (key/3) */
|
||||||
|
uint8_t fp = 0;
|
||||||
|
if (App_Auto_NeedRegisterFix()) fp |= 0x01u;
|
||||||
|
if (App_Auto_XposeExtra()) fp |= 0x02u;
|
||||||
|
if (KEY_ID_1629 >= 1 && KEY_ID_1629 <= 21)
|
||||||
|
fp |= (uint8_t)(((KEY_ID_1629 - 1) / 3) << 2);
|
||||||
|
return fp;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void App_Auto_QueueAllNotesOffMelodic(void)
|
||||||
|
{
|
||||||
|
MidiFifoItem_t m;
|
||||||
|
int ch;
|
||||||
|
for (ch = 0; ch < 16; ch++)
|
||||||
|
{
|
||||||
|
if (ch == DRUM_CH) continue;
|
||||||
|
m.msg[0] = (uint8_t)(0xB0 | ch);
|
||||||
|
m.msg[1] = 123; /* All Notes Off */
|
||||||
|
m.msg[2] = 0;
|
||||||
|
m.msglen = 3;
|
||||||
|
mymidififo_InQueue(&m_MidiSendFifo, &m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 超出 #G4:在当前和弦内音(GetChordNotesByType,对照和弦表「和弦伴奏」列)中就近 */
|
||||||
|
static int App_Auto_NearestChordTone(int target)
|
||||||
|
{
|
||||||
|
uint8_t buff[4] = {0, 0, 0, 0};
|
||||||
|
uint8_t n;
|
||||||
|
uint8_t pcs[4];
|
||||||
|
int i, oct, best = -1, best_dist = 9999;
|
||||||
|
uint8_t type;
|
||||||
|
|
||||||
|
if (KEY_ID_1629 < 1 || KEY_ID_1629 > 21)
|
||||||
|
return App_Auto_ClampMidi(target);
|
||||||
|
|
||||||
|
type = chord_type_index_map[KEY_ID_1629].type;
|
||||||
|
n = GetChordNotesByType(KEY_ID_1629, type, buff);
|
||||||
|
if (n == 0)
|
||||||
|
return App_Auto_ClampMidi(target > MIDI_GSHARP4 ? MIDI_GSHARP4 : target);
|
||||||
|
|
||||||
|
for (i = 0; i < (int)n; i++)
|
||||||
|
pcs[i] = (uint8_t)(buff[i] % 12);
|
||||||
|
|
||||||
|
for (i = 0; i < (int)n; i++)
|
||||||
|
{
|
||||||
|
for (oct = 0; oct < 11; oct++)
|
||||||
|
{
|
||||||
|
int cand = (int)pcs[i] + oct * 12;
|
||||||
|
int dist;
|
||||||
|
if (cand < MIDI_E2 || cand > MIDI_GSHARP4)
|
||||||
|
continue;
|
||||||
|
dist = cand - target;
|
||||||
|
if (dist < 0) dist = -dist;
|
||||||
|
/* 并列:优先较低者(不超过上限) */
|
||||||
|
if (dist < best_dist || (dist == best_dist && (best < 0 || cand < best)))
|
||||||
|
{
|
||||||
|
best_dist = dist;
|
||||||
|
best = cand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best < 0)
|
||||||
|
return MIDI_GSHARP4;
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_MapBassNote(int key, uint8_t *flags)
|
||||||
|
{
|
||||||
|
int out = key - 12;
|
||||||
|
uint8_t f = 0x01u; /* bit0: -12 applied */
|
||||||
|
while (out < MIDI_E1)
|
||||||
|
{
|
||||||
|
out += 12;
|
||||||
|
f |= 0x02u; /* bit1: floored up to E1 */
|
||||||
|
}
|
||||||
|
if (App_Auto_XposeExtra())
|
||||||
|
{
|
||||||
|
out -= 12;
|
||||||
|
f |= 0x04u; /* bit2: xpose extra -12 */
|
||||||
|
while (out < MIDI_E1)
|
||||||
|
{
|
||||||
|
out += 12;
|
||||||
|
f |= 0x02u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flags) *flags = f;
|
||||||
|
return App_Auto_ClampMidi(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_MapChordNote(int key, uint8_t *flags)
|
||||||
|
{
|
||||||
|
int out = key - 12;
|
||||||
|
uint8_t f = 0x01u; /* bit0: -12 */
|
||||||
|
|
||||||
|
while (out < MIDI_E2)
|
||||||
|
{
|
||||||
|
out += 12;
|
||||||
|
f |= 0x08u; /* bit3: raised to E2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out > MIDI_GSHARP4)
|
||||||
|
{
|
||||||
|
out = App_Auto_NearestChordTone(out);
|
||||||
|
f |= 0x10u; /* bit4: nearest chord tone */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (App_Auto_XposeExtra())
|
||||||
|
{
|
||||||
|
out -= 12;
|
||||||
|
f |= 0x04u;
|
||||||
|
while (out < MIDI_E2)
|
||||||
|
{
|
||||||
|
out += 12;
|
||||||
|
f |= 0x08u;
|
||||||
|
}
|
||||||
|
if (out > MIDI_GSHARP4)
|
||||||
|
{
|
||||||
|
out = App_Auto_NearestChordTone(out);
|
||||||
|
f |= 0x10u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flags) *flags = f;
|
||||||
|
return App_Auto_ClampMidi(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_MapNote(int channel, int key, uint8_t *flags)
|
||||||
|
{
|
||||||
|
int out = key;
|
||||||
|
if (flags) *flags = 0;
|
||||||
|
|
||||||
|
#if CHORD_REG_FIX_EN
|
||||||
|
if (channel == DRUM_CH || key < 0 || key > 127)
|
||||||
|
return key;
|
||||||
|
|
||||||
|
if (!App_Auto_NeedRegisterFix())
|
||||||
|
return key;
|
||||||
|
|
||||||
|
if (channel == BASS_CH)
|
||||||
|
out = App_Auto_MapBassNote(key, flags);
|
||||||
|
else
|
||||||
|
out = App_Auto_MapChordNote(key, flags);
|
||||||
|
#else
|
||||||
|
(void)channel;
|
||||||
|
#endif
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *App_Auto_RoleName(int channel)
|
||||||
|
{
|
||||||
|
if (channel == DRUM_CH) return "drum";
|
||||||
|
if (channel == BASS_CH) return "bass";
|
||||||
|
return "chord";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void App_Auto_LogPitch(int channel, int key, int out, uint8_t flags, int is_on)
|
||||||
|
{
|
||||||
|
const char *role;
|
||||||
|
int deg;
|
||||||
|
int fix;
|
||||||
|
int xf;
|
||||||
|
int pass;
|
||||||
|
|
||||||
|
if (channel == DRUM_CH)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (channel == BASS_CH)
|
||||||
|
{
|
||||||
|
if (s_bass_log_left == 0)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (s_pitch_log_left == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
role = App_Auto_RoleName(channel);
|
||||||
|
deg = App_Auto_ChordDegree();
|
||||||
|
fix = App_Auto_NeedRegisterFix();
|
||||||
|
xf = App_Auto_XposeExtra();
|
||||||
|
pass = (!fix || channel == DRUM_CH) ? 1 : 0;
|
||||||
|
|
||||||
|
LOG_I("PITCH", "%s ch%u(%s) %d->%d d=%d deg=%d chord=%u xp=%u xf=%d fl=0x%02X %s%s%s%s",
|
||||||
|
is_on ? "on" : "off",
|
||||||
|
(unsigned)channel, role, /* 0 基,与 BASS_CH=8 一致 */
|
||||||
|
key, out, out - key,
|
||||||
|
deg, (unsigned)KEY_ID_1629,
|
||||||
|
(unsigned)mGuiData[GUI_TRANSPOSE].Current, xf,
|
||||||
|
(unsigned)flags,
|
||||||
|
pass ? "PASS " : "",
|
||||||
|
(flags & 0x10u) ? "NEAR " : "",
|
||||||
|
(flags & 0x02u) ? "E1UP " : "",
|
||||||
|
(flags & 0x08u) ? "E2UP " : "");
|
||||||
|
|
||||||
|
if (channel == BASS_CH)
|
||||||
|
{
|
||||||
|
if (s_bass_log_left > 0)
|
||||||
|
s_bass_log_left--;
|
||||||
|
}
|
||||||
|
else if (s_pitch_log_left > 0)
|
||||||
|
{
|
||||||
|
s_pitch_log_left--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NoteOn 路径:策略变化时清旧音;返回映射后音高 */
|
||||||
|
static int App_Auto_MapNoteOn(int channel, int key)
|
||||||
|
{
|
||||||
|
#if CHORD_REG_FIX_EN
|
||||||
|
uint8_t fp = App_Auto_RegFingerprint();
|
||||||
|
uint8_t flags = 0;
|
||||||
|
int out;
|
||||||
|
|
||||||
|
if (fp != s_reg_fp)
|
||||||
|
{
|
||||||
|
if (s_reg_fp != 0xFFu)
|
||||||
|
{
|
||||||
|
LOG_I("REG", "fp %u->%u chord=%u deg=%d xp=%u xf=%d",
|
||||||
|
(unsigned)s_reg_fp, (unsigned)fp,
|
||||||
|
(unsigned)KEY_ID_1629,
|
||||||
|
App_Auto_ChordDegree(),
|
||||||
|
(unsigned)mGuiData[GUI_TRANSPOSE].Current,
|
||||||
|
App_Auto_XposeExtra());
|
||||||
|
App_Auto_QueueAllNotesOffMelodic();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG_I("REG", "fp init %u chord=%u deg=%d xp=%u",
|
||||||
|
(unsigned)fp, (unsigned)KEY_ID_1629,
|
||||||
|
App_Auto_ChordDegree(),
|
||||||
|
(unsigned)mGuiData[GUI_TRANSPOSE].Current);
|
||||||
|
}
|
||||||
|
s_reg_fp = fp;
|
||||||
|
s_pitch_log_left = PITCH_LOG_PER_FP;
|
||||||
|
s_bass_log_left = BASS_LOG_PER_FP;
|
||||||
|
s_ch_ever_mask = 0; /* 新策略周期重新报到通道 */
|
||||||
|
}
|
||||||
|
|
||||||
|
out = App_Auto_MapNote(channel, key, &flags);
|
||||||
|
if (channel >= 0 && channel < 16)
|
||||||
|
{
|
||||||
|
uint16_t bit = (uint16_t)(1u << channel);
|
||||||
|
if ((s_ch_ever_mask & bit) == 0u)
|
||||||
|
{
|
||||||
|
s_ch_ever_mask |= bit;
|
||||||
|
LOG_I("REG", "ch-seen ch%u role=%s key=%d",
|
||||||
|
(unsigned)channel, App_Auto_RoleName(channel), key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
App_Auto_LogPitch(channel, key, out, flags, 1);
|
||||||
|
return out;
|
||||||
|
#else
|
||||||
|
(void)channel;
|
||||||
|
return key;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
static void Func_CallBack_ProgramChange(int channel, int program)
|
static void Func_CallBack_ProgramChange(int channel, int program)
|
||||||
{
|
{
|
||||||
MidiFifoItem_t midimsg;
|
MidiFifoItem_t midimsg;
|
||||||
midimsg.msg[0] = 0xc0 | channel;
|
LOG_I("REG", "pc ch%u prog=%d", (unsigned)channel, program);
|
||||||
|
midimsg.msg[0] = 0xc0 | (channel & 0x0F);
|
||||||
midimsg.msg[1] = program;
|
midimsg.msg[1] = program;
|
||||||
midimsg.msg[2] = 0;
|
midimsg.msg[2] = 0;
|
||||||
midimsg.msglen = 2;
|
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)
|
static void Func_CallBack_NoteOff(int channel,int key)
|
||||||
{
|
{
|
||||||
MidiFifoItem_t midimsg;
|
MidiFifoItem_t midimsg;
|
||||||
midimsg.msg[0] = 0x80 | channel;
|
uint8_t flags = 0;
|
||||||
midimsg.msg[1] = key;
|
int out_key = App_Auto_MapNote(channel, key, &flags);
|
||||||
|
midimsg.msg[0] = 0x80 | (channel & 0x0F);
|
||||||
|
midimsg.msg[1] = (uint8_t)out_key;
|
||||||
midimsg.msg[2] = 0;
|
midimsg.msg[2] = 0;
|
||||||
midimsg.msglen = 3;
|
midimsg.msglen = 3;
|
||||||
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
||||||
|
|
@ -43,9 +353,19 @@ static void Func_CallBack_NoteOff(int channel,int key)
|
||||||
static void Func_CallBack_NoteOn(int channel,int key,int vel)
|
static void Func_CallBack_NoteOn(int channel,int key,int vel)
|
||||||
{
|
{
|
||||||
MidiFifoItem_t midimsg;
|
MidiFifoItem_t midimsg;
|
||||||
midimsg.msg[0] = 0x90 | channel;
|
int out_key;
|
||||||
midimsg.msg[1] = key;
|
|
||||||
midimsg.msg[2] = vel;
|
if (vel == 0)
|
||||||
|
{
|
||||||
|
uint8_t flags = 0;
|
||||||
|
out_key = App_Auto_MapNote(channel, key, &flags); /* 视同 NoteOff */
|
||||||
|
}
|
||||||
|
else
|
||||||
|
out_key = App_Auto_MapNoteOn(channel, key);
|
||||||
|
|
||||||
|
midimsg.msg[0] = 0x90 | (channel & 0x0F);
|
||||||
|
midimsg.msg[1] = (uint8_t)out_key;
|
||||||
|
midimsg.msg[2] = (uint8_t)vel;
|
||||||
midimsg.msglen = 3;
|
midimsg.msglen = 3;
|
||||||
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
||||||
|
|
||||||
|
|
@ -135,6 +455,9 @@ static int Func_CallBack_ReadFlash(int address,int length,uint8_t* pOutput)
|
||||||
//
|
//
|
||||||
void App_Auto_Init(void)
|
void App_Auto_Init(void)
|
||||||
{
|
{
|
||||||
|
LOG_I("REG", "ch map bass=%d drum=%d fix=%d E1=%d E2=%d Gs4=%d",
|
||||||
|
BASS_CH, DRUM_CH, CHORD_REG_FIX_EN, MIDI_E1, MIDI_E2, MIDI_GSHARP4);
|
||||||
|
LOG_I("REG", "PITCH log: chN(role) in->out d=delta deg=I..VII xp=xpose xf=#Fextra fl=flags");
|
||||||
AutoBandTop1_SetPresetStoreBuffer(pStoreBuffer,PRESET_BUFFER_SIZE);
|
AutoBandTop1_SetPresetStoreBuffer(pStoreBuffer,PRESET_BUFFER_SIZE);
|
||||||
AutoBandTop1_Init();
|
AutoBandTop1_Init();
|
||||||
AutoBandTop1_RegisterCallBack_NoteOn(Func_CallBack_NoteOn);
|
AutoBandTop1_RegisterCallBack_NoteOn(Func_CallBack_NoteOn);
|
||||||
|
|
@ -148,8 +471,12 @@ void App_Auto_Init(void)
|
||||||
int ret = AutoBandTop1_LoadPresetItemFromFlash(0);
|
int ret = AutoBandTop1_LoadPresetItemFromFlash(0);
|
||||||
if (ret != 0)
|
if (ret != 0)
|
||||||
{
|
{
|
||||||
LOG_E("AUTO", "LoadPreset fail ret=%d addr=0x%08X map=%s",
|
uint8_t hdr[16];
|
||||||
ret, (unsigned)ADDRESS, ToneFlashMapBank((uint32_t)ADDRESS));
|
W25Q128_Read(hdr, (uint32_t)ADDRESS, 16);
|
||||||
|
LOG_E("AUTO", "LoadPreset fail ret=%d addr=0x%08X map=%s magic=%02X%02X%02X%02X cnt=%lu",
|
||||||
|
ret, (unsigned)ADDRESS, ToneFlashMapBank((uint32_t)ADDRESS),
|
||||||
|
hdr[0], hdr[1], hdr[2], hdr[3],
|
||||||
|
(unsigned long)(hdr[12] | (hdr[13] << 8) | (hdr[14] << 16) | (hdr[15] << 24)));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
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,
|
LCD_WR_PIC_FROM_FLASH(UI0902_MODE_ROW_X, y,
|
||||||
UI0902_MODE_ROW_W, UI0902_MODE_ROW_H,
|
UI0902_MODE_ROW_W, UI0902_MODE_ROW_H,
|
||||||
s_mode_row_addr[i]);
|
s_mode_row_addr[i]);
|
||||||
/* 万能模式素材顶部有残留近白横线(相对行内 y=7/8),盖成行底色 */
|
|
||||||
if (i == 0) {
|
|
||||||
LCD_FillByColor(UI0902_MODE_ROW_X, (uint16_t)(y + 7),
|
|
||||||
(uint16_t)(UI0902_MODE_ROW_X + UI0902_MODE_ROW_W),
|
|
||||||
(uint16_t)(y + 9), UI0902_ROW_BG);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,18 +163,19 @@ void IdleProcess(DisplayTaskMessage_Type msg)
|
||||||
TM1629D_UpdateDisplay(0);
|
TM1629D_UpdateDisplay(0);
|
||||||
StartTouchTask();
|
StartTouchTask();
|
||||||
StartScanTask();
|
StartScanTask();
|
||||||
/* Dream 上电稍后可能忽略首包 SysEx:再发主音量 + 调音台(保持关机前设置) */
|
/* Dream 上电稍后可能忽略首包 SysEx:再发主音量 + 调音台(保持关机前设置)。
|
||||||
|
* 此时扫描任务已启动、模拟开关在轮询,不可现场重读 ADC(会串通道),
|
||||||
|
* 统一用 MasterVolume_Reapply 取扫描任务维护的最近一次有效音量 */
|
||||||
rt_thread_mdelay(400);
|
rt_thread_mdelay(400);
|
||||||
vol = ADC_ReadChannel(ADC_CHANNEL_1)>>5;
|
MasterVolume_Reapply();
|
||||||
app_adc_sync_volume(vol);
|
|
||||||
Send_volume(vol);
|
|
||||||
LoadConfig();
|
LoadConfig();
|
||||||
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, level, 1, WHITE, GRAY);
|
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, level, 1, WHITE, GRAY);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MSG_ID_POWER_OFF:
|
case MSG_ID_POWER_OFF:
|
||||||
/* 关机:熄屏 + 关背光 */
|
/* 无 Type-C:整机关机熄屏;有 Type-C 时随后 POWOFF_CHARG 会再亮充电图 */
|
||||||
LCD_BLK_Clr();
|
LCD_BLK_Clr();
|
||||||
|
LCD_WR_REG(0x28); /* Display OFF */
|
||||||
LCD_FillByColor(0, 0, 240, 320, BLACK);
|
LCD_FillByColor(0, 0, 240, 320, BLACK);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -190,6 +185,8 @@ void IdleProcess(DisplayTaskMessage_Type msg)
|
||||||
|
|
||||||
case MSG_ID_TOUCH:
|
case MSG_ID_TOUCH:
|
||||||
{
|
{
|
||||||
|
if (!powon)
|
||||||
|
break; /* 软关机已停触摸;兜底勿进模式选择 */
|
||||||
static uint32_t s_last_enter_ms;
|
static uint32_t s_last_enter_ms;
|
||||||
uint32_t now = rt_tick_get() * 1000U / RT_TICK_PER_SECOND;
|
uint32_t now = rt_tick_get() * 1000U / RT_TICK_PER_SECOND;
|
||||||
uint8_t hit = 0U;
|
uint8_t hit = 0U;
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,33 @@ void DrawAllFaders(void)
|
||||||
drv_nvm_save_to_flash();
|
drv_nvm_save_to_flash();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 0902-06:实心蓝色圆钮(勿用同心空心圆,会透出色条) */
|
/* 0902-06:圆钮定位半径 = 色条半宽;填充略大以盖住柱端抗锯齿白边(夹紧不画出柱外) */
|
||||||
#define MIXER_KNOB_R 14
|
#define MIXER_KNOB_R 15
|
||||||
|
#define MIXER_KNOB_FILL_R 16
|
||||||
|
#define MIXER_TRACK_HALF_W 15
|
||||||
|
|
||||||
|
/* 与柱端半圆同形的实心圆钮:顶/底档位与色条圆角完美贴合,无虚线白边 */
|
||||||
|
static void DrawMixerKnob(uint16_t cx, uint16_t cy)
|
||||||
|
{
|
||||||
|
int16_t dy;
|
||||||
|
int32_t r2 = (int32_t)MIXER_KNOB_FILL_R * (int32_t)MIXER_KNOB_FILL_R;
|
||||||
|
for (dy = -(int16_t)MIXER_KNOB_R; dy <= (int16_t)MIXER_KNOB_R; dy++)
|
||||||
|
{
|
||||||
|
int32_t xx = r2 - (int32_t)dy * (int32_t)dy;
|
||||||
|
int16_t dx = 0;
|
||||||
|
int16_t half;
|
||||||
|
if (xx < 0)
|
||||||
|
continue;
|
||||||
|
while ((int32_t)(dx + 1) * (dx + 1) <= xx)
|
||||||
|
dx++;
|
||||||
|
half = dx;
|
||||||
|
if (half > (int16_t)MIXER_TRACK_HALF_W)
|
||||||
|
half = (int16_t)MIXER_TRACK_HALF_W;
|
||||||
|
LCD_FillByColor((uint16_t)(cx - half), (uint16_t)(cy + dy),
|
||||||
|
(uint16_t)(cx + half + 1), (uint16_t)(cy + dy + 1),
|
||||||
|
COLOR_GRAD_MID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void DrawFaderSlider(Fader_t *f, uint16_t slider_y)
|
static void DrawFaderSlider(Fader_t *f, uint16_t slider_y)
|
||||||
{
|
{
|
||||||
|
|
@ -62,9 +87,9 @@ static void DrawFaderSlider(Fader_t *f, uint16_t slider_y)
|
||||||
uint16_t ly = f->y + f->h + 8;
|
uint16_t ly = f->y + f->h + 8;
|
||||||
uint16_t cy = (uint16_t)(slider_y + MIXER_KNOB_R);
|
uint16_t cy = (uint16_t)(slider_y + MIXER_KNOB_R);
|
||||||
char buf[8];
|
char buf[8];
|
||||||
LCD_FillCircle(cx, cy, MIXER_KNOB_R, COLOR_GRAD_MID);
|
DrawMixerKnob(cx, cy);
|
||||||
sprintf(buf, "%d", f->val);
|
sprintf(buf, "%d", f->val);
|
||||||
LCD_ShowString_AutoAlign((uint16_t)(cx - 8), (uint16_t)(cx + 8),
|
LCD_ShowString_AutoAlign((uint16_t)(cx - 10), (uint16_t)(cx + 10),
|
||||||
(uint16_t)(cy - 5),
|
(uint16_t)(cy - 5),
|
||||||
(uint8_t*)buf, LCD_ALIGN_CENTER, WHITE, COLOR_GRAD_MID, 0, 11);
|
(uint8_t*)buf, LCD_ALIGN_CENTER, WHITE, COLOR_GRAD_MID, 0, 11);
|
||||||
LCD_FillByColor(cx - 15, ly + 17, cx + 15, ly + 19, UI0902_BG_COLOR);
|
LCD_FillByColor(cx - 15, ly + 17, cx + 15, ly + 19, UI0902_BG_COLOR);
|
||||||
|
|
@ -149,17 +174,14 @@ void UpdateFaderByTouch(int idx, uint16_t y)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ??????Y????????val? 0~10??
|
// 滑块顶边 Y:val0/10 时圆心落在色条底/顶半圆中心,两端对称贴合
|
||||||
// ============================================================
|
// ============================================================
|
||||||
static uint16_t CalcSliderY(Fader_t *f)
|
static uint16_t CalcSliderY(Fader_t *f)
|
||||||
{
|
{
|
||||||
uint16_t slider_h = (uint16_t)(MIXER_KNOB_R * 2);
|
uint16_t top_cy = (uint16_t)(f->y + MIXER_KNOB_R);
|
||||||
uint16_t fill_h = (f->val * f->h) / 10;
|
uint16_t bot_cy = (uint16_t)(f->y + f->h - 1 - MIXER_KNOB_R);
|
||||||
if (fill_h > f->h) fill_h = f->h;
|
uint16_t cy = (uint16_t)(bot_cy - ((uint32_t)(bot_cy - top_cy) * f->val) / 10);
|
||||||
uint16_t slider_y = f->y + f->h - fill_h - (slider_h / 2);
|
return (uint16_t)(cy - MIXER_KNOB_R);
|
||||||
if (slider_y < f->y) slider_y = f->y;
|
|
||||||
if (slider_y > f->y + f->h - slider_h) slider_y = f->y + f->h - slider_h;
|
|
||||||
return slider_y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int CheckTouchFader(uint16_t x, uint16_t y)
|
int CheckTouchFader(uint16_t x, uint16_t y)
|
||||||
|
|
@ -222,7 +244,7 @@ void UI_Mixer_Process(DisplayTaskMessage_Type msg)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MSG_ID_EC_VOL:
|
case MSG_ID_EC_VOL:
|
||||||
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, msg.HiByte, 1, WHITE, GRAY);
|
/* 调音台子页无状态栏音量条(与返回键重叠);主音量已在 MainTask 下发 */
|
||||||
ResetAutoPowerCount();
|
ResetAutoPowerCount();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,8 @@ void Touch_Action(int8_t FLAG, uint8_t ID, uint8_t areaIndex,uint8_t value)
|
||||||
AutoBandTop1_SetRunVar(mGuiData[ID].Current);
|
AutoBandTop1_SetRunVar(mGuiData[ID].Current);
|
||||||
}else
|
}else
|
||||||
{
|
{
|
||||||
|
/* 触摸起奏前补发主音量,保证实际音量与图标一致 */
|
||||||
|
MasterVolume_Reapply();
|
||||||
AutoBandTop1_FillinWithIndex(value);
|
AutoBandTop1_FillinWithIndex(value);
|
||||||
AutoBandTop1_Start();
|
AutoBandTop1_Start();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ const Touch_AreaTypeDef Touch_SetingSelect_Areas[] = {
|
||||||
{ UI0902_HUB_CARD_X, (uint16_t)(UI0902_HUB_CARD_X + UI0902_HUB_CARD_W - 1),
|
{ UI0902_HUB_CARD_X, (uint16_t)(UI0902_HUB_CARD_X + UI0902_HUB_CARD_W - 1),
|
||||||
UI0902_HUB_CARD_Y1, (uint16_t)(UI0902_HUB_CARD_Y1 + UI0902_HUB_CARD_H - 1),
|
UI0902_HUB_CARD_Y1, (uint16_t)(UI0902_HUB_CARD_Y1 + UI0902_HUB_CARD_H - 1),
|
||||||
0, 0, GUI_SETING_SELECT3, NULL },
|
0, 0, GUI_SETING_SELECT3, NULL },
|
||||||
/* 底栏四键 */
|
/* 底栏四键:与 Touch_Areas 共用 UI0902_NAV_* 几何 */
|
||||||
{ 0, 69, 280, 319, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(0), UI0902_NAV_X1(0), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
|
||||||
{ 70, 129, 280, 319, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(1), UI0902_NAV_X1(1), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
|
||||||
{ 130, 184, 280, 319, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(2), UI0902_NAV_X1(2), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
|
||||||
{ 185, 239, 280, 319, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
|
{ UI0902_NAV_X0(3), UI0902_NAV_X1(3), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
|
||||||
};
|
};
|
||||||
|
|
||||||
#define TOUCH_AREA_SETING_SELECT_NUM (sizeof(Touch_SetingSelect_Areas)/sizeof(Touch_AreaTypeDef))
|
#define TOUCH_AREA_SETING_SELECT_NUM (sizeof(Touch_SetingSelect_Areas)/sizeof(Touch_AreaTypeDef))
|
||||||
|
|
@ -124,6 +124,11 @@ void UI_SystemSet_Process(DisplayTaskMessage_Type msg)
|
||||||
label_top();
|
label_top();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case MSG_ID_EC_VOL:
|
||||||
|
/* 主音量已在 MainTask 下发;此处只刷新状态栏音量条,保证与旋钮一致 */
|
||||||
|
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, msg.HiByte, 1, WHITE, GRAY);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
191
UI/UI_global.c
|
|
@ -54,6 +54,7 @@ void CallUI_Idle(void) {
|
||||||
|
|
||||||
void CallUI_SongMode(void) {
|
void CallUI_SongMode(void) {
|
||||||
app_touch_suppress(500);
|
app_touch_suppress(500);
|
||||||
|
MasterVolume_Reapply(); /* 进模式补发主音量,防开机 SysEx 丢失后实际音量与图标不符 */
|
||||||
UI_SongMode_Init();
|
UI_SongMode_Init();
|
||||||
CurrUIProcress = UI_SongMode_Process;
|
CurrUIProcress = UI_SongMode_Process;
|
||||||
app_log_set_ui_page("Song");
|
app_log_set_ui_page("Song");
|
||||||
|
|
@ -62,6 +63,7 @@ void CallUI_SongMode(void) {
|
||||||
|
|
||||||
void CallUI_ExpertMode(void) {
|
void CallUI_ExpertMode(void) {
|
||||||
app_touch_suppress(500);
|
app_touch_suppress(500);
|
||||||
|
MasterVolume_Reapply();
|
||||||
UI_ExpertMode_Init();
|
UI_ExpertMode_Init();
|
||||||
// CurrUIProcress = UI_ExpertMode_Process;
|
// CurrUIProcress = UI_ExpertMode_Process;
|
||||||
CurrUIProcress = UI_SongMode_Process;
|
CurrUIProcress = UI_SongMode_Process;
|
||||||
|
|
@ -71,6 +73,7 @@ void CallUI_ExpertMode(void) {
|
||||||
|
|
||||||
void CallUI_FreeMode(void) {
|
void CallUI_FreeMode(void) {
|
||||||
app_touch_suppress(500);
|
app_touch_suppress(500);
|
||||||
|
MasterVolume_Reapply();
|
||||||
UI_FreeMode_Init();
|
UI_FreeMode_Init();
|
||||||
// CurrUIProcress = UI_FreeMode_Process;
|
// CurrUIProcress = UI_FreeMode_Process;
|
||||||
CurrUIProcress = UI_SongMode_Process;
|
CurrUIProcress = UI_SongMode_Process;
|
||||||
|
|
@ -168,10 +171,23 @@ void UI_ReloadTonePreset(void)
|
||||||
if (mGuiData[GUI_TAB_INDEX].Current == 0)
|
if (mGuiData[GUI_TAB_INDEX].Current == 0)
|
||||||
{
|
{
|
||||||
int ret = AutoBandTop1_LoadPresetItemFromFlash(ParamGuiData[ALL_MODE_PARAM].Current);
|
int ret = AutoBandTop1_LoadPresetItemFromFlash(ParamGuiData[ALL_MODE_PARAM].Current);
|
||||||
LOG_I("tone", "load universal idx=%d ret=%d name=%s addr=0x%08X map=BIN3@0x%08X",
|
if (ret != 0)
|
||||||
ParamGuiData[ALL_MODE_PARAM].Current, ret,
|
{
|
||||||
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
uint8_t hdr[16];
|
||||||
(unsigned)ADDRESS, (unsigned)EXTFLASH_BIN3_UNIVERSAL_ADDR);
|
W25Q128_Read(hdr, (uint32_t)ADDRESS, 16);
|
||||||
|
LOG_E("tone", "load universal idx=%d ret=%d magic=%02X%02X%02X%02X cnt=%lu addr=0x%08X",
|
||||||
|
ParamGuiData[ALL_MODE_PARAM].Current, ret,
|
||||||
|
hdr[0], hdr[1], hdr[2], hdr[3],
|
||||||
|
(unsigned long)(hdr[12] | (hdr[13] << 8) | (hdr[14] << 16) | (hdr[15] << 24)),
|
||||||
|
(unsigned)ADDRESS);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG_I("tone", "load universal idx=%d ret=%d name=%s addr=0x%08X map=BIN3@0x%08X",
|
||||||
|
ParamGuiData[ALL_MODE_PARAM].Current, ret,
|
||||||
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
||||||
|
(unsigned)ADDRESS, (unsigned)EXTFLASH_BIN3_UNIVERSAL_ADDR);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/* 普通/专业:本地曲目或节奏类型 */
|
/* 普通/专业:本地曲目或节奏类型 */
|
||||||
|
|
@ -443,6 +459,58 @@ void Show_Battery_Icon(uint8_t flag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 中英文混排像素宽(与 LCD_GetMixedStringWidth 规则一致:CJK=sizey,ASCII=UI0902_ASCII_W) */
|
||||||
|
static uint16_t UI_MixedTextWidth(const uint8_t *str, uint8_t sizey)
|
||||||
|
{
|
||||||
|
uint16_t width = 0;
|
||||||
|
uint8_t ascii_w = UI0902_ASCII_W(sizey);
|
||||||
|
|
||||||
|
if (str == NULL)
|
||||||
|
return 0;
|
||||||
|
while (*str != '\0') {
|
||||||
|
if (*str > 0x80) {
|
||||||
|
width = (uint16_t)(width + sizey);
|
||||||
|
str += 2;
|
||||||
|
} else {
|
||||||
|
width = (uint16_t)(width + ascii_w);
|
||||||
|
str += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 曲目/音色共用:序号与名称间距(与「1.名称」拼接一致,保证两行视觉间距相同) */
|
||||||
|
#define UI0902_INDEX_NAME_GAP 0
|
||||||
|
|
||||||
|
/* 在 [x1,x2] 内整体居中绘制「N.」+ 名称(无渐变,show_Param 用) */
|
||||||
|
static void UI_DrawIndexedName_Mixed(uint16_t x1, uint16_t x2, uint16_t text_y,
|
||||||
|
const char *num, const char *name,
|
||||||
|
uint16_t fc, uint16_t bc, uint8_t sizey)
|
||||||
|
{
|
||||||
|
char line[48] = {0};
|
||||||
|
snprintf(line, sizeof(line), "%s%s", num, name);
|
||||||
|
LCD_ShowMixedString(x1, x2, text_y, (uint8_t *)line, LCD_ALIGN_CENTER,
|
||||||
|
fc, bc, sizey, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 在 [x1,x2] 内整体居中绘制「N.」+ 中文名(渐变底,音色行用);间隙与曲目行一致 */
|
||||||
|
static void UI_DrawIndexedName_Grad(uint16_t x1, uint16_t x2, uint16_t text_y,
|
||||||
|
const char *num, const uint8_t *name,
|
||||||
|
uint16_t fc, uint16_t c_top, uint16_t c_bot, uint8_t sizey)
|
||||||
|
{
|
||||||
|
uint16_t num_w = (uint16_t)(strlen(num) * UI0902_ASCII_W(sizey));
|
||||||
|
uint16_t name_w = UI_MixedTextWidth(name, sizey);
|
||||||
|
uint16_t total_w = (uint16_t)(num_w + UI0902_INDEX_NAME_GAP + name_w);
|
||||||
|
uint16_t area_w = (x2 > x1) ? (uint16_t)(x2 - x1) : 0;
|
||||||
|
uint16_t start_x = (total_w >= area_w) ? x1 : (uint16_t)(x1 + (area_w - total_w) / 2);
|
||||||
|
uint16_t name_x = (uint16_t)(start_x + num_w + UI0902_INDEX_NAME_GAP);
|
||||||
|
|
||||||
|
LCD_ShowString_AutoAlign_Gradient(start_x, (uint16_t)(start_x + num_w), text_y,
|
||||||
|
(const uint8_t *)num, LCD_ALIGN_LEFT, fc, c_top, c_bot, sizey);
|
||||||
|
LCD_ShowChinese_AutoAlign_Gradient(name_x, x2, text_y, name, LCD_ALIGN_LEFT,
|
||||||
|
fc, c_top, c_bot, sizey);
|
||||||
|
}
|
||||||
|
|
||||||
void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
||||||
{
|
{
|
||||||
char Textstr_new[40] = {0};
|
char Textstr_new[40] = {0};
|
||||||
|
|
@ -453,9 +521,19 @@ void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
||||||
const uint16_t text_y = row_y + (row_h - UI->Text_size) / 2 + 1;
|
const uint16_t text_y = row_y + (row_h - UI->Text_size) / 2 + 1;
|
||||||
uint8_t idx = Group->Current + 1;
|
uint8_t idx = Group->Current + 1;
|
||||||
uint16_t c_top, c_bot;
|
uint16_t c_top, c_bot;
|
||||||
|
uint16_t x1, x2;
|
||||||
|
|
||||||
UI_GetFocusGrad(GUI_MODE_PARAM, &c_top, &c_bot);
|
UI_GetFocusGrad(GUI_MODE_PARAM, &c_top, &c_bot);
|
||||||
|
|
||||||
|
x1 = (uint16_t)(UI->x + 10);
|
||||||
|
x2 = (uint16_t)(UI->x + UI->w - 10);
|
||||||
|
|
||||||
|
/* 修复曲目名尾部残留:旧名比新名长时,只填渐变胶囊(x+25 ~ x+w-40)
|
||||||
|
盖不住文字区(x+10 ~ x+w-10)内的旧笔画,切到短名后尾部留残影。
|
||||||
|
先按行底色清整个文字区,再补渐变与文字;整行清除会盖住两侧三角,
|
||||||
|
文字绘制后统一补画。 */
|
||||||
|
LCD_FillByColor(x1, row_y, x2, (uint16_t)(row_y + row_h), c_top);
|
||||||
|
|
||||||
LCD_FillRoundRectGradient(
|
LCD_FillRoundRectGradient(
|
||||||
UI->x+25, row_y,
|
UI->x+25, row_y,
|
||||||
UI->x + UI->w-40-(UI->x+25),
|
UI->x + UI->w-40-(UI->x+25),
|
||||||
|
|
@ -464,18 +542,21 @@ void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
||||||
c_top, c_bot
|
c_top, c_bot
|
||||||
);
|
);
|
||||||
|
|
||||||
// 拼接序号 1. 2. 3.
|
/* 序号+名称整体居中(约等于标注「右移7px」的视觉效果) */
|
||||||
sprintf(NumBuf, "%d.", idx);
|
sprintf(NumBuf, "%d.", idx);
|
||||||
uint16_t num_w = (uint16_t)(strlen(NumBuf) * UI0902_ASCII_W(UI->Text_size));
|
|
||||||
if (Group->Id == EXPRESS_MODE_PARAM && Group->Current < LOCAL_SONG_COUNT)
|
if (Group->Id == EXPRESS_MODE_PARAM && Group->Current < LOCAL_SONG_COUNT)
|
||||||
strcpy(Textstr_new, LocalSongNameGbk[Group->Current]);
|
strcpy(Textstr_new, LocalSongNameGbk[Group->Current]);
|
||||||
else
|
else
|
||||||
GET_ParamNum_Str(Group->Current, 0, Textstr_new, Group->Id);
|
GET_ParamNum_Str(Group->Current, 0, Textstr_new, Group->Id);
|
||||||
uint32_t string_start_x = LCD_ShowMixedString(UI->x+10, UI->x + UI->w, text_y, (uint8_t *)Textstr_new , LCD_ALIGN_CENTER, WHITE, BLACK, UI->Text_size, 1);
|
UI_DrawIndexedName_Mixed(x1, x2, text_y, NumBuf, Textstr_new,
|
||||||
//序号X = 中文起点 - 数字宽度 - 5,间<EFBC8C>?像素
|
WHITE, BLACK, UI->Text_size);
|
||||||
uint16_t num_x = string_start_x - num_w;
|
|
||||||
// 绘制左侧数字序号
|
/* 补画左右三角(几何与 label_ModeSelece_ui 一致) */
|
||||||
LCD_ShowString(num_x, text_y, (const uint8_t *)NumBuf, UI->Text_fc, UI->Text_bc, UI->Text_size, 1);
|
{
|
||||||
|
uint16_t row_cy = (uint16_t)(row_y + row_h / 2);
|
||||||
|
UI_DrawTriLeft((uint16_t)(UI->x + 5), row_cy);
|
||||||
|
UI_DrawTriRight((uint16_t)(UI->x + UI->w - 5 - UI0902_TRI_R_W), row_cy);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -803,12 +884,8 @@ void label_ModeSelece_ui(const LEBEL_UI *UI,GUI_SWITCH * Group)
|
||||||
paramCurData = &ParamGuiData[SONG_MODE_PARAM];
|
paramCurData = &ParamGuiData[SONG_MODE_PARAM];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 左右三角由 show_Param 末尾统一补画(整行清除后必须重画) */
|
||||||
show_Param(&UI_Label[GUI_MODE_PARAM], paramCurData);
|
show_Param(&UI_Label[GUI_MODE_PARAM], paramCurData);
|
||||||
{
|
|
||||||
uint16_t row_cy = UI->y + UI0902_TAB_H + 4 + (UI->h - UI0902_TAB_H - 4) / 2;
|
|
||||||
UI_DrawTriLeft(UI->x + 5, row_cy);
|
|
||||||
UI_DrawTriRight((uint16_t)(UI->x + UI->w - 5 - UI0902_TRI_R_W), row_cy);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void label_TimbreSelece_ui(const LEBEL_UI *UI, GUI_SWITCH * Group)
|
void label_TimbreSelece_ui(const LEBEL_UI *UI, GUI_SWITCH * Group)
|
||||||
|
|
@ -851,22 +928,17 @@ void label_TimbreSelece_ui(const LEBEL_UI *UI, GUI_SWITCH * Group)
|
||||||
c_top, c_bot
|
c_top, c_bot
|
||||||
);
|
);
|
||||||
sprintf(NumBuf, "%d.", idx);
|
sprintf(NumBuf, "%d.", idx);
|
||||||
uint16_t num_w = (uint16_t)(strlen(NumBuf) * UI0902_ASCII_W(UI->Text_size));
|
|
||||||
GET_Num_Str(mGuiData[UI->Id].Current, 0, (char*)Textstr, UI->Id);
|
GET_Num_Str(mGuiData[UI->Id].Current, 0, (char*)Textstr, UI->Id);
|
||||||
uint32_t chinese_start_x = LCD_ShowChinese_AutoAlign_Gradient(
|
UI_DrawIndexedName_Grad(
|
||||||
UI->x + num_w,
|
(uint16_t)(UI->x + 10),
|
||||||
UI->x + UI->w,
|
(uint16_t)(UI->x + UI->w - 10),
|
||||||
text_y,
|
text_y,
|
||||||
(const uint8_t*)Textstr,
|
NumBuf,
|
||||||
UI->Text_Align,
|
Textstr,
|
||||||
UI->Text_fc,
|
UI->Text_fc,
|
||||||
c_top, c_bot,
|
c_top, c_bot,
|
||||||
UI->Text_size
|
UI->Text_size
|
||||||
);
|
);
|
||||||
uint16_t num_x = chinese_start_x - num_w - 5;
|
|
||||||
LCD_ShowString_AutoAlign_Gradient(num_x, num_x + num_w, text_y,
|
|
||||||
(const uint8_t *)NumBuf, LCD_ALIGN_LEFT, UI->Text_fc,
|
|
||||||
c_top, c_bot, UI->Text_size);
|
|
||||||
switch(Group->Id)
|
switch(Group->Id)
|
||||||
{
|
{
|
||||||
case GUI_TIMBRE_SELECT:
|
case GUI_TIMBRE_SELECT:
|
||||||
|
|
@ -907,37 +979,32 @@ void UI_DrawSubPageHeader(const uint8_t *title_gbk)
|
||||||
|
|
||||||
void Draw_Bottom_Bar(uint8_t sel, uint8_t first_back)
|
void Draw_Bottom_Bar(uint8_t sel, uint8_t first_back)
|
||||||
{
|
{
|
||||||
/* 需求稿四键:万能 | 普通 | 专业 | 设置;first_back 时设置位仍高亮(返回用触控逻辑) */
|
/* 四键合成图(图标+原稿文字);统一 32 高,文字底边对齐 */
|
||||||
const uint8_t *icons[4];
|
const uint8_t *icons[4];
|
||||||
uint8_t ws[4], hs[4];
|
uint8_t ws[4];
|
||||||
/* 0904:四键中心约 40 / 100 / 160 / 210 */
|
|
||||||
const uint16_t cx[4] = { 40, 100, 160, 210 };
|
const uint16_t cx[4] = { 40, 100, 160, 210 };
|
||||||
|
const uint8_t icon_h = 32;
|
||||||
|
const uint16_t icon_y = 282;
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
|
|
||||||
(void)first_back;
|
(void)first_back;
|
||||||
/* 从段落钮底边之下清屏,勿切掉按钮底圆角(钮 y=231..264) */
|
|
||||||
LCD_FillByColor(0, (uint16_t)(UI0902_SECTION_Y + UI0902_SECTION_BTN_H),
|
LCD_FillByColor(0, (uint16_t)(UI0902_SECTION_Y + UI0902_SECTION_BTN_H),
|
||||||
240, 320, UI0902_BG_COLOR);
|
240, 320, UI0902_BG_COLOR);
|
||||||
/* mode.png:段落钮与底栏之间横线分隔(与顶栏同色同厚) */
|
|
||||||
LCD_FillByColor(0, UI0902_SEP_LINE_Y, 240,
|
LCD_FillByColor(0, UI0902_SEP_LINE_Y, 240,
|
||||||
(uint16_t)(UI0902_SEP_LINE_Y + UI0902_SEP_LINE_THICK),
|
(uint16_t)(UI0902_SEP_LINE_Y + UI0902_SEP_LINE_THICK),
|
||||||
UI0902_SEP_LINE_COLOR);
|
UI0902_SEP_LINE_COLOR);
|
||||||
|
|
||||||
if (sel == UI0902_NAV_UNIVERSAL) { icons[0] = gImage_Nav_Universal_Sel; ws[0] = 36; hs[0] = 28; }
|
icons[0] = (sel == UI0902_NAV_UNIVERSAL) ? gImage_Nav_Universal_Sel : gImage_Nav_Universal_Not;
|
||||||
else { icons[0] = gImage_Nav_Universal_Not; ws[0] = 36; hs[0] = 28; }
|
icons[1] = (sel == UI0902_NAV_NORMAL) ? gImage_Nav_Normal_Sel : gImage_Nav_Normal_Not;
|
||||||
|
icons[2] = (sel == UI0902_NAV_EXPERT) ? gImage_Nav_Expert_Sel : gImage_Nav_Expert_Not;
|
||||||
if (sel == UI0902_NAV_NORMAL) { icons[1] = gImage_Nav_Normal_Sel; ws[1] = 35; hs[1] = 29; }
|
icons[3] = (sel == UI0902_NAV_SETTING) ? gImage_Nav_Setting_Sel : gImage_Nav_Setting_Not;
|
||||||
else { icons[1] = gImage_Nav_Normal_Not; ws[1] = 35; hs[1] = 28; }
|
ws[0] = ws[1] = ws[2] = 40;
|
||||||
|
ws[3] = 22;
|
||||||
if (sel == UI0902_NAV_EXPERT) { icons[2] = gImage_Nav_Expert_Sel; ws[2] = 35; hs[2] = 27; }
|
|
||||||
else { icons[2] = gImage_Nav_Expert_Not; ws[2] = 35; hs[2] = 27; }
|
|
||||||
|
|
||||||
if (sel == UI0902_NAV_SETTING) { icons[3] = gImage_Nav_Setting_Sel; ws[3] = 18; hs[3] = 28; }
|
|
||||||
else { icons[3] = gImage_Nav_Setting_Not; ws[3] = 18; hs[3] = 28; }
|
|
||||||
|
|
||||||
for (i = 0; i < 4; i++)
|
for (i = 0; i < 4; i++)
|
||||||
{
|
{
|
||||||
LCD_WR_PIC_Trans(cx[i] - ws[i] / 2, 283, ws[i], hs[i], icons[i], BLACK);
|
LCD_WR_PIC_Trans((uint16_t)(cx[i] - ws[i] / 2), icon_y,
|
||||||
|
ws[i], icon_h, icons[i], BLACK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -988,7 +1055,7 @@ void Draw_Tab_Section_Menu(const LEBEL_UI *UI, TAB_SECTION_INDEX select_tab)
|
||||||
const uint8_t pitch = UI0902_SECTION_PITCH;
|
const uint8_t pitch = UI0902_SECTION_PITCH;
|
||||||
const uint16_t start_x = UI0902_MARGIN_X; /* 0902-04:12/69/126/183 */
|
const uint16_t start_x = UI0902_MARGIN_X; /* 0902-04:12/69/126/183 */
|
||||||
const uint16_t start_y = UI->y;
|
const uint16_t start_y = UI->y;
|
||||||
/* 0819:演奏段落数字与标题同为 13,按钮内垂直居中(视觉重心 +1) */
|
/* 0819:演奏段落数字与标题同为 13,按钮内垂直居中(视觉重心 +1);稿面数字整体右移 2px */
|
||||||
const uint16_t text_y = start_y + (tab_h - UI0902_FONT_BODY) / 2 + 1;
|
const uint16_t text_y = start_y + (tab_h - UI0902_FONT_BODY) / 2 + 1;
|
||||||
const char *nums[4] = {"1", "2", "3", "4"};
|
const char *nums[4] = {"1", "2", "3", "4"};
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
|
|
@ -999,7 +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;
|
uint16_t bg = (select_tab == i) ? COLOR_GRAD_MID : UI0902_ROW_BG;
|
||||||
LCD_FillRoundRect(x, start_y, tab_w, tab_h, tab_r, bg);
|
LCD_FillRoundRect(x, start_y, tab_w, tab_h, tab_r, bg);
|
||||||
LCD_ShowString_AutoAlign(
|
LCD_ShowString_AutoAlign(
|
||||||
x, (uint16_t)(x + tab_w - 1), text_y,
|
(uint16_t)(x + 2), (uint16_t)(x + tab_w - 1 + 2), text_y,
|
||||||
(const uint8_t*)nums[i],
|
(const uint8_t*)nums[i],
|
||||||
UI->Text_Align,
|
UI->Text_Align,
|
||||||
WHITE,
|
WHITE,
|
||||||
|
|
@ -1223,11 +1290,11 @@ void Draw_System_Item_List(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case GUI_BLUETOOTH_SW:
|
case GUI_BLUETOOTH_SW:
|
||||||
/* 素材:系统设置图标-11(白蓝牙符);Flash 槽名 RESTORE,尺寸 13x21 */
|
/* 素材:系统设置图标-11(白蓝牙符)→ ICON_SET_BLUETOOTH 13x21 */
|
||||||
LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12,
|
LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12,
|
||||||
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_RESTORE_H) / 2),
|
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SET_BLUETOOTH_H) / 2),
|
||||||
UI0902_ICON_SYS_RESTORE_W, UI0902_ICON_SYS_RESTORE_H,
|
UI0902_ICON_SET_BLUETOOTH_W, UI0902_ICON_SET_BLUETOOTH_H,
|
||||||
UI0902_ICON_SYS_RESTORE_ADDR);
|
UI0902_ICON_SET_BLUETOOTH_ADDR);
|
||||||
UI_DrawTriLeft(val_x1, chev_cy);
|
UI_DrawTriLeft(val_x1, chev_cy);
|
||||||
if(Group->Current)
|
if(Group->Current)
|
||||||
LCD_ShowChinese_AutoAlign_Gradient(val_x1 + UI0902_TRI_L_W, val_x2 - UI0902_TRI_R_W, text_y,(const uint8_t*)"\xCA\xC7",LCD_ALIGN_CENTER, WHITE,c_top, c_bot,value_sz);
|
LCD_ShowChinese_AutoAlign_Gradient(val_x1 + UI0902_TRI_L_W, val_x2 - UI0902_TRI_R_W, text_y,(const uint8_t*)"\xCA\xC7",LCD_ALIGN_CENTER, WHITE,c_top, c_bot,value_sz);
|
||||||
|
|
@ -1264,13 +1331,17 @@ void Draw_System_Item_List(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
||||||
drv_nvm_save_to_flash();
|
drv_nvm_save_to_flash();
|
||||||
break;
|
break;
|
||||||
case GUI_RESTORE:
|
case GUI_RESTORE:
|
||||||
/* 逆时针箭头:当前在 VERSION 槽(19x18);RESTORE 槽曾误为蓝牙符 */
|
/* 素材:系统设置图标-13(逆时针恢复)→ ICON_SYS_RESTORE 19x18 */
|
||||||
LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12,
|
LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12,
|
||||||
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_VERSION_H) / 2),
|
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_RESTORE_H) / 2),
|
||||||
UI0902_ICON_SYS_VERSION_W, UI0902_ICON_SYS_VERSION_H,
|
UI0902_ICON_SYS_RESTORE_W, UI0902_ICON_SYS_RESTORE_H,
|
||||||
UI0902_ICON_SYS_VERSION_ADDR);
|
UI0902_ICON_SYS_RESTORE_ADDR);
|
||||||
/* 右三角与蓝牙/自动关机同一右缘(val_x2) */
|
/* 0902-07:右侧为进入下级页的细 chevron(设置图标-03),勿用实心三角 */
|
||||||
UI_DrawTriRight((uint16_t)(val_x2 - UI0902_TRI_R_W), chev_cy);
|
LCD_WR_PIC_FROM_FLASH_Trans(
|
||||||
|
(uint16_t)(val_x2 - UI0902_ICON_CHEVRON_W),
|
||||||
|
(uint16_t)(UI->y + (UI->h - UI0902_ICON_CHEVRON_H) / 2),
|
||||||
|
UI0902_ICON_CHEVRON_W, UI0902_ICON_CHEVRON_H,
|
||||||
|
UI0902_ICON_CHEVRON_ADDR);
|
||||||
break;
|
break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,19 +173,25 @@ void CallUI_RestoreSelect(void);
|
||||||
#define UI0902_NAV_EXPERT 2 /* 底栏:专业模式 */
|
#define UI0902_NAV_EXPERT 2 /* 底栏:专业模式 */
|
||||||
#define UI0902_NAV_SETTING 3 /* 底栏:设置 */
|
#define UI0902_NAV_SETTING 3 /* 底栏:设置 */
|
||||||
#define UI0902_NAV_NONE 0xFF
|
#define UI0902_NAV_NONE 0xFF
|
||||||
|
/* 底栏触区:段落钮底(y264)之下四等分全宽,各 60×55 */
|
||||||
|
#define UI0902_NAV_Y0 265
|
||||||
|
#define UI0902_NAV_Y1 319
|
||||||
|
#define UI0902_NAV_W 60
|
||||||
|
#define UI0902_NAV_X0(i) ((uint16_t)((i) * UI0902_NAV_W))
|
||||||
|
#define UI0902_NAV_X1(i) ((uint16_t)((i) * UI0902_NAV_W + UI0902_NAV_W - 1))
|
||||||
/* 兼容旧名(已废弃三键:设置|调音台|模式) */
|
/* 兼容旧名(已废弃三键:设置|调音台|模式) */
|
||||||
#define UI0902_NAV_MIXER UI0902_NAV_NORMAL
|
#define UI0902_NAV_MIXER UI0902_NAV_NORMAL
|
||||||
#define UI0902_NAV_MODE UI0902_NAV_EXPERT
|
#define UI0902_NAV_MODE UI0902_NAV_EXPERT
|
||||||
|
|
||||||
/* 底部图标实际内容(生成脚本曾把 万能/普通/专业/设置 误命名为 Setting/Mixer/Mode) */
|
/* 底部图标:0909 加大图标 + 原稿文字条(合成图);生成脚本曾误命名 Setting/Mixer/Mode */
|
||||||
#define gImage_Nav_Universal_Not gImage_UI0902_TabSetting_Sel_36x28 /* 底部-10 白 */
|
#define gImage_Nav_Universal_Not gImage_UI0902_TabSetting_Sel_40x32 /* 底部-10 白 */
|
||||||
#define gImage_Nav_Universal_Sel gImage_UI0902_TabMode_Sel_36x28 /* 底部-14 蓝 */
|
#define gImage_Nav_Universal_Sel gImage_UI0902_TabMode_Sel_40x32 /* 底部-14 蓝 */
|
||||||
#define gImage_Nav_Normal_Not gImage_UI0902_TabSetting_Not_35x28 /* 底部-11 白 */
|
#define gImage_Nav_Normal_Not gImage_UI0902_TabSetting_Not_40x32 /* 底部-11 白 */
|
||||||
#define gImage_Nav_Normal_Sel gImage_UI0902_TabMode_Not_35x29 /* 底部-15 蓝 */
|
#define gImage_Nav_Normal_Sel gImage_UI0902_TabMode_Not_40x32 /* 底部-15 蓝 */
|
||||||
#define gImage_Nav_Expert_Not gImage_UI0902_TabMixer_Sel_35x27 /* 底部-12 白 */
|
#define gImage_Nav_Expert_Not gImage_UI0902_TabMixer_Sel_40x32 /* 底部-12 白 */
|
||||||
#define gImage_Nav_Expert_Sel gImage_UI0902_TabBack_Sel_35x27 /* 底部-16 蓝 */
|
#define gImage_Nav_Expert_Sel gImage_UI0902_TabBack_Sel_40x32 /* 底部-16 蓝 */
|
||||||
#define gImage_Nav_Setting_Not gImage_UI0902_TabMixer_Not_18x28 /* 底部-13 白 */
|
#define gImage_Nav_Setting_Not gImage_UI0902_TabMixer_Not_22x32 /* 底部-13 白 */
|
||||||
#define gImage_Nav_Setting_Sel gImage_UI0902_TabBack_Not_18x28 /* 底部-17 蓝 */
|
#define gImage_Nav_Setting_Sel gImage_UI0902_TabBack_Not_22x32 /* 底部-17 蓝 */
|
||||||
/* 旧几何电池(Draw_Battery_Icon)最左起点;0902 图电池用 Draw_Status_Battery_Icon */
|
/* 旧几何电池(Draw_Battery_Icon)最左起点;0902 图电池用 Draw_Status_Battery_Icon */
|
||||||
#define UI_STATUS_BATTERY_X 150
|
#define UI_STATUS_BATTERY_X 150
|
||||||
#define UI_STATUS_BATTERY_H 11
|
#define UI_STATUS_BATTERY_H 11
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,15 @@ extern const unsigned char gImage_UI0902_BatteryOutline_29x12[];
|
||||||
extern const unsigned char gImage_UI0902_Volume_20x15[600];
|
extern const unsigned char gImage_UI0902_Volume_20x15[600];
|
||||||
extern const unsigned char gImage_UI0902_Bluetooth_11x17[374];
|
extern const unsigned char gImage_UI0902_Bluetooth_11x17[374];
|
||||||
extern const unsigned char gImage_UI0902_BatteryFill_22x9[396];
|
extern const unsigned char gImage_UI0902_BatteryFill_22x9[396];
|
||||||
extern const unsigned char gImage_UI0902_TabSetting_Sel_36x28[2016];
|
extern const unsigned char gImage_UI0902_TabSetting_Sel_40x32[2560];
|
||||||
extern const unsigned char gImage_UI0902_TabSetting_Not_35x28[1960];
|
extern const unsigned char gImage_UI0902_TabSetting_Not_40x32[2560];
|
||||||
extern const unsigned char gImage_UI0902_TabMixer_Sel_35x27[1890];
|
extern const unsigned char gImage_UI0902_TabMixer_Sel_40x32[2560];
|
||||||
extern const unsigned char gImage_UI0902_TabMixer_Not_18x28[1008];
|
extern const unsigned char gImage_UI0902_TabMixer_Not_22x32[1408];
|
||||||
extern const unsigned char gImage_UI0902_TabMixer_Not_35x28[1960];
|
extern const unsigned char gImage_UI0902_TabMixer_Not_35x28[1960];
|
||||||
extern const unsigned char gImage_UI0902_TabMode_Sel_36x28[2016];
|
extern const unsigned char gImage_UI0902_TabMode_Sel_40x32[2560];
|
||||||
extern const unsigned char gImage_UI0902_TabMode_Not_35x29[2030];
|
extern const unsigned char gImage_UI0902_TabMode_Not_40x32[2560];
|
||||||
extern const unsigned char gImage_UI0902_TabBack_Sel_35x27[1890];
|
extern const unsigned char gImage_UI0902_TabBack_Sel_40x32[2560];
|
||||||
extern const unsigned char gImage_UI0902_TabBack_Not_18x28[1008];
|
extern const unsigned char gImage_UI0902_TabBack_Not_22x32[1408];
|
||||||
|
|
||||||
extern const unsigned char gImage_UI0902_BtnMinus_23x22[1012];
|
extern const unsigned char gImage_UI0902_BtnMinus_23x22[1012];
|
||||||
extern const unsigned char gImage_UI0902_BtnPlus_23x22[1012];
|
extern const unsigned char gImage_UI0902_BtnPlus_23x22[1012];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// File: w25q128.c
|
// File: w25q128.c
|
||||||
#include "w25q128.h"
|
#include "w25q128.h"
|
||||||
#include "wk_system.h"
|
#include "wk_system.h"
|
||||||
|
#include "rtthread.h"
|
||||||
|
|
||||||
/* W25Q128 挂在 SPI1 上(PA5=SCK, PA6=MISO, PA7=MOSI),CS 用 PA4 GPIO 控制 */
|
/* W25Q128 挂在 SPI1 上(PA5=SCK, PA6=MISO, PA7=MOSI),CS 用 PA4 GPIO 控制 */
|
||||||
#define W25Q128_SPI SPI1
|
#define W25Q128_SPI SPI1
|
||||||
|
|
@ -82,6 +83,12 @@ static void W25Q128_WaitBusy(void)
|
||||||
W25Q128_ReadWriteByte(W25X_ReadStatusReg1); // 发送读状态寄存器命令
|
W25Q128_ReadWriteByte(W25X_ReadStatusReg1); // 发送读状态寄存器命令
|
||||||
do {
|
do {
|
||||||
status = W25Q128_ReadWriteByte(0xFF); // 循环读取状态字
|
status = W25Q128_ReadWriteByte(0xFF); // 循环读取状态字
|
||||||
|
if (status & 0x01) {
|
||||||
|
SPI_CS_HIGH();
|
||||||
|
rt_thread_mdelay(1);
|
||||||
|
SPI_CS_LOW();
|
||||||
|
W25Q128_ReadWriteByte(W25X_ReadStatusReg1);
|
||||||
|
}
|
||||||
} while (status & 0x01); // 等待BUSY位清零
|
} while (status & 0x01); // 等待BUSY位清零
|
||||||
SPI_CS_HIGH();
|
SPI_CS_HIGH();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,3 +37,22 @@ void SendMidiDataToDreamDSP(uint8_t* buff, uint16_t cnt)
|
||||||
{
|
{
|
||||||
USART2_SendData(buff,cnt);
|
USART2_SendData(buff,cnt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define AT32_UID_BASE 0x1FFFF7E8UL
|
||||||
|
|
||||||
|
void BleBuildDeviceName(char *out)
|
||||||
|
{
|
||||||
|
static const char hex[] = "0123456789ABCDEF";
|
||||||
|
const uint32_t *uid = (const uint32_t *)AT32_UID_BASE;
|
||||||
|
uint32_t tail = uid[2] & 0x00FFFFFFu;
|
||||||
|
unsigned i;
|
||||||
|
|
||||||
|
out[0] = 'P';
|
||||||
|
out[1] = 'H';
|
||||||
|
out[2] = 'O';
|
||||||
|
out[3] = 'N';
|
||||||
|
out[4] = '_';
|
||||||
|
for (i = 0; i < 6; i++)
|
||||||
|
out[5 + i] = hex[(tail >> (4 * (5 - i))) & 0xFu];
|
||||||
|
out[BLE_DEVICE_NAME_LEN] = '\0';
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,8 @@ void SendVolumeChangeMidiEvent(uint8_t channel, uint8_t CC);
|
||||||
void SendMidiDataToDreamDSP(uint8_t* buff, uint16_t cnt);
|
void SendMidiDataToDreamDSP(uint8_t* buff, uint16_t cnt);
|
||||||
void DefaultTask_SendMsg(uint16_t Data1, uint16_t Data2, uint16_t Data3, uint16_t Data4);
|
void DefaultTask_SendMsg(uint16_t Data1, uint16_t Data2, uint16_t Data3, uint16_t Data4);
|
||||||
|
|
||||||
|
/* Unique BLE/BT name from AT32 UID: "PHON_" + 6 hex (last 3 UID bytes). out >= 12. */
|
||||||
|
#define BLE_DEVICE_NAME_LEN 11
|
||||||
|
void BleBuildDeviceName(char *out);
|
||||||
|
|
||||||
#endif /* __MIDI_SEND_H__ */
|
#endif /* __MIDI_SEND_H__ */
|
||||||
|
|
|
||||||
|
|
@ -663,7 +663,7 @@
|
||||||
<name>BUILDACTION</name>
|
<name>BUILDACTION</name>
|
||||||
<archiveVersion>1</archiveVersion>
|
<archiveVersion>1</archiveVersion>
|
||||||
<data>
|
<data>
|
||||||
<prebuild></prebuild>
|
<prebuild>python "$PROJ_DIR$\..\..\tools\gen_git_user_fw_ver.py"</prebuild>
|
||||||
<postbuild></postbuild>
|
<postbuild></postbuild>
|
||||||
</data>
|
</data>
|
||||||
</settings>
|
</settings>
|
||||||
|
|
@ -1650,7 +1650,7 @@
|
||||||
<name>BUILDACTION</name>
|
<name>BUILDACTION</name>
|
||||||
<archiveVersion>1</archiveVersion>
|
<archiveVersion>1</archiveVersion>
|
||||||
<data>
|
<data>
|
||||||
<prebuild></prebuild>
|
<prebuild>python "$PROJ_DIR$\..\..\tools\gen_git_user_fw_ver.py"</prebuild>
|
||||||
<postbuild></postbuild>
|
<postbuild></postbuild>
|
||||||
</data>
|
</data>
|
||||||
</settings>
|
</settings>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
#define EXTFLASH_CHARGING_H 190
|
#define EXTFLASH_CHARGING_H 190
|
||||||
|
|
||||||
#define EXTFLASH_BIN1_RHYTHM_ADDR 0x0001B8F0UL
|
#define EXTFLASH_BIN1_RHYTHM_ADDR 0x0001B8F0UL
|
||||||
#define EXTFLASH_BIN1_RHYTHM_SIZE 530317UL
|
#define EXTFLASH_BIN1_RHYTHM_SIZE 530273UL
|
||||||
|
|
||||||
#define EXTFLASH_BIN2_SONG_HAITIAN_ADDR 0x0009D07DUL
|
#define EXTFLASH_BIN2_SONG_HAITIAN_ADDR 0x0009D07DUL
|
||||||
#define EXTFLASH_BIN2_SONG_HAITIAN_SIZE 41263UL
|
#define EXTFLASH_BIN2_SONG_HAITIAN_SIZE 41263UL
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
void USART2_SendData(uint8_t *data, uint16_t len);
|
void USART2_SendData(uint8_t *data, uint16_t len);
|
||||||
void USART4_SendData(uint8_t *data, uint16_t len);
|
void USART4_SendData(uint8_t *data, uint16_t len);
|
||||||
|
|
||||||
|
/* 等待 UART4 发送环形缓冲排空(供关机 ACK 等场景) */
|
||||||
|
void USART4_WaitTxIdle(uint32_t timeout_ms);
|
||||||
|
|
||||||
/* 接收出队:返回 1=取到数据,0=队列空 */
|
/* 接收出队:返回 1=取到数据,0=队列空 */
|
||||||
uint8_t USART2_RxPop(uint8_t *c);
|
uint8_t USART2_RxPop(uint8_t *c);
|
||||||
uint8_t UART4_RxPop(uint8_t *c);
|
uint8_t UART4_RxPop(uint8_t *c);
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ int main(void)
|
||||||
/* add user code begin 3 */
|
/* add user code begin 3 */
|
||||||
Power_Key_Scan();
|
Power_Key_Scan();
|
||||||
AutoPowerOff_Scan();
|
AutoPowerOff_Scan();
|
||||||
|
System_PowerOff_Poll();
|
||||||
LCD_Dump_Poll();
|
LCD_Dump_Poll();
|
||||||
#if !DEBUG_LCD_DUMP
|
#if !DEBUG_LCD_DUMP
|
||||||
app_log_poll();
|
app_log_poll();
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
|
|
||||||
#define BUFF_SIZE 512
|
#define BUFF_SIZE 512
|
||||||
|
|
||||||
/* 纯比较宏:参数必须是普通局部变量(非 volatile),
|
/* ?????????????????????????????? volatile????
|
||||||
避免同一表达式多次访问 volatile 触发 Pa082 且逻辑不稳 */
|
?????????????????? volatile ???? Pa082 ????????? */
|
||||||
#define IS_FULL(head, tail) ((((head) + 1) % BUFF_SIZE) == (tail))
|
#define IS_FULL(head, tail) ((((head) + 1) % BUFF_SIZE) == (tail))
|
||||||
#define IS_EMPTY(head, tail) ((head) == (tail))
|
#define IS_EMPTY(head, tail) ((head) == (tail))
|
||||||
|
|
||||||
/* ==================== 环形队列结构体 ==================== */
|
/* ==================== ???????????? ==================== */
|
||||||
|
|
||||||
/* 一个串口实例的环形队列(收发各一) */
|
/* ????????????????????????????? */
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
uint8_t rx_buff[BUFF_SIZE];
|
uint8_t rx_buff[BUFF_SIZE];
|
||||||
|
|
@ -21,14 +21,14 @@ typedef struct
|
||||||
volatile uint16_t tx_head;
|
volatile uint16_t tx_head;
|
||||||
volatile uint16_t tx_tail;
|
volatile uint16_t tx_tail;
|
||||||
|
|
||||||
usart_type *usart; /* 关联的串口外设(USART2 / UART4) */
|
usart_type *usart; /* ??????????????USART2 / UART4?? */
|
||||||
} uart_ring_t;
|
} uart_ring_t;
|
||||||
|
|
||||||
/* USART2 和 UART4 各一个实例 */
|
/* USART2 ?? UART4 ???????? */
|
||||||
static uart_ring_t uart2;
|
static uart_ring_t uart2;
|
||||||
static uart_ring_t uart4;
|
static uart_ring_t uart4;
|
||||||
|
|
||||||
/* 初始化:关联串口外设(由 wk_usart2_init / wk_uart4_init 调用) */
|
/* ?????????????????????? wk_usart2_init / wk_uart4_init ????? */
|
||||||
void uart_ring_usart2_init(void)
|
void uart_ring_usart2_init(void)
|
||||||
{
|
{
|
||||||
uart2.usart = USART2;
|
uart2.usart = USART2;
|
||||||
|
|
@ -39,19 +39,19 @@ void uart_ring_uart4_init(void)
|
||||||
uart4.usart = UART4;
|
uart4.usart = UART4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 通用入队/出队(带实例参数) ==================== */
|
/* ==================== ??????/???????????????? ==================== */
|
||||||
|
|
||||||
/* 入队(ISR 调用)。先快照 volatile 值再操作,避免 Pa082 */
|
/* ????ISR ???????????? volatile ???????????? Pa082 */
|
||||||
static void ring_rx_push(uart_ring_t *rb, uint8_t c)
|
static void ring_rx_push(uart_ring_t *rb, uint8_t c)
|
||||||
{
|
{
|
||||||
uint16_t h = rb->rx_head;
|
uint16_t h = rb->rx_head;
|
||||||
uint16_t t = rb->rx_tail;
|
uint16_t t = rb->rx_tail;
|
||||||
if(IS_FULL(h, t)) return; /* 满则丢,防止覆盖未读数据 */
|
if(IS_FULL(h, t)) return; /* ???????????????????? */
|
||||||
rb->rx_buff[h] = c;
|
rb->rx_buff[h] = c;
|
||||||
rb->rx_head = (h + 1) % BUFF_SIZE;
|
rb->rx_head = (h + 1) % BUFF_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 出队(业务线程调用) */
|
/* ??????????????? */
|
||||||
static uint8_t ring_rx_pop(uart_ring_t *rb, uint8_t *c)
|
static uint8_t ring_rx_pop(uart_ring_t *rb, uint8_t *c)
|
||||||
{
|
{
|
||||||
uint16_t h = rb->rx_head;
|
uint16_t h = rb->rx_head;
|
||||||
|
|
@ -62,65 +62,81 @@ static uint8_t ring_rx_pop(uart_ring_t *rb, uint8_t *c)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 写入 tx 队列(调用前应已关中断,防止 ISR 同时读) */
|
/* ???? tx ??????????????????????? ISR ?????? */
|
||||||
static void ring_tx_write(uart_ring_t *rb, uint8_t *data, uint16_t len)
|
static void ring_tx_write(uart_ring_t *rb, uint8_t *data, uint16_t len)
|
||||||
{
|
{
|
||||||
uint16_t h = rb->tx_head;
|
uint16_t h = rb->tx_head;
|
||||||
uint16_t t = rb->tx_tail;
|
uint16_t t = rb->tx_tail;
|
||||||
for(uint16_t i = 0; i < len; i++)
|
for(uint16_t i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
if(IS_FULL(h, t)) break; /* 用 tx 判满 */
|
if(IS_FULL(h, t)) break; /* ?? tx ???? */
|
||||||
rb->tx_buff[h] = data[i];
|
rb->tx_buff[h] = data[i];
|
||||||
h = (h + 1) % BUFF_SIZE;
|
h = (h + 1) % BUFF_SIZE;
|
||||||
}
|
}
|
||||||
rb->tx_head = h; /* 最后一次性写回 volatile */
|
rb->tx_head = h; /* ???????????? volatile */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 发送接口(带串口参数) ==================== */
|
/* ==================== ?????????????????? ==================== */
|
||||||
|
|
||||||
/* 通用发送:关中断 → 写队列 → 开 TDBE 中断触发发送 */
|
/* ?????????????? ?? ?????? ?? ?? TDBE ??????????? */
|
||||||
static void uart_ring_send(uart_ring_t *rb, uint8_t *data, uint16_t len)
|
static void uart_ring_send(uart_ring_t *rb, uint8_t *data, uint16_t len)
|
||||||
{
|
{
|
||||||
rt_base_t level = rt_hw_interrupt_disable();
|
rt_base_t level = rt_hw_interrupt_disable();
|
||||||
|
|
||||||
ring_tx_write(rb, data, len);
|
ring_tx_write(rb, data, len);
|
||||||
usart_interrupt_enable(rb->usart, USART_TDBE_INT, TRUE); /* 开发送中断 */
|
usart_interrupt_enable(rb->usart, USART_TDBE_INT, TRUE); /* ?????????? */
|
||||||
|
|
||||||
rt_hw_interrupt_enable(level);
|
rt_hw_interrupt_enable(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* USART2 发送(对外,midi_send.c 等在用) */
|
/* USART2 ?????????midi_send.c ??????? */
|
||||||
void USART2_SendData(uint8_t *data, uint16_t len)
|
void USART2_SendData(uint8_t *data, uint16_t len)
|
||||||
{
|
{
|
||||||
uart_ring_send(&uart2, data, len);
|
uart_ring_send(&uart2, data, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* UART4 发送(对外) */
|
/* UART4 ????????? */
|
||||||
void USART4_SendData(uint8_t *data, uint16_t len)
|
void USART4_SendData(uint8_t *data, uint16_t len)
|
||||||
{
|
{
|
||||||
uart_ring_send(&uart4, data, len);
|
uart_ring_send(&uart4, data, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* USART2 接收出队(对外) */
|
void USART4_WaitTxIdle(uint32_t timeout_ms)
|
||||||
|
{
|
||||||
|
uint32_t t0 = rt_tick_get();
|
||||||
|
while ((rt_tick_get() - t0) < rt_tick_from_millisecond(timeout_ms ? timeout_ms : 1))
|
||||||
|
{
|
||||||
|
uint16_t h = uart4.tx_head;
|
||||||
|
uint16_t t = uart4.tx_tail;
|
||||||
|
if (IS_EMPTY(h, t))
|
||||||
|
{
|
||||||
|
/* <20><><EFBFBD><EFBFBD>պ<EFBFBD><D5BA>ٵ<EFBFBD><D9B5><EFBFBD>λ<EFBFBD>Ĵ<EFBFBD><C4B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> */
|
||||||
|
if (usart_flag_get(uart4.usart, USART_TDC_FLAG) == SET)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rt_thread_mdelay(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* USART2 ???????????? */
|
||||||
uint8_t USART2_RxPop(uint8_t *c)
|
uint8_t USART2_RxPop(uint8_t *c)
|
||||||
{
|
{
|
||||||
return ring_rx_pop(&uart2, c);
|
return ring_rx_pop(&uart2, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* UART4 接收出队(对外) */
|
/* UART4 ???????????? */
|
||||||
uint8_t UART4_RxPop(uint8_t *c)
|
uint8_t UART4_RxPop(uint8_t *c)
|
||||||
{
|
{
|
||||||
return ring_rx_pop(&uart4, c);
|
return ring_rx_pop(&uart4, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 串口中断 ==================== */
|
/* ==================== ???????? ==================== */
|
||||||
|
|
||||||
static void uart_ring_isr(uart_ring_t *rb)
|
static void uart_ring_isr(uart_ring_t *rb)
|
||||||
{
|
{
|
||||||
/* 接收:收到字节 → 入 rx 队列 */
|
|
||||||
if(usart_flag_get(rb->usart, USART_RDBF_FLAG) == SET)
|
if(usart_flag_get(rb->usart, USART_RDBF_FLAG) == SET)
|
||||||
{
|
{
|
||||||
uint8_t data = usart_data_receive(rb->usart); /* 读数据(自动清 RDBF) */
|
uint8_t data = usart_data_receive(rb->usart);
|
||||||
ring_rx_push(rb, data);
|
ring_rx_push(rb, data);
|
||||||
if(rb->usart == UART4)
|
if(rb->usart == UART4)
|
||||||
{
|
{
|
||||||
|
|
@ -128,7 +144,6 @@ static void uart_ring_isr(uart_ring_t *rb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 发送:tx 队列有数据 → 发一个字节;发完 → 关 TDBE 中断 */
|
|
||||||
if(usart_flag_get(rb->usart, USART_TDBE_FLAG) == SET)
|
if(usart_flag_get(rb->usart, USART_TDBE_FLAG) == SET)
|
||||||
{
|
{
|
||||||
uint16_t h = rb->tx_head;
|
uint16_t h = rb->tx_head;
|
||||||
|
|
@ -140,11 +155,10 @@ static void uart_ring_isr(uart_ring_t *rb)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
usart_interrupt_enable(rb->usart, USART_TDBE_INT, FALSE); /* 发完关中断 */
|
usart_interrupt_enable(rb->usart, USART_TDBE_INT, FALSE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 错误处理:清溢出标志,防止反复进中断卡死 */
|
|
||||||
if(usart_flag_get(rb->usart, USART_ROERR_FLAG) == SET)
|
if(usart_flag_get(rb->usart, USART_ROERR_FLAG) == SET)
|
||||||
usart_flag_clear(rb->usart, USART_ROERR_FLAG);
|
usart_flag_clear(rb->usart, USART_ROERR_FLAG);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "includes.h"
|
#include "includes.h"
|
||||||
|
#include "git_user_fw_ver.h"
|
||||||
|
|
||||||
|
|
||||||
// Э<><D0AD>֡ͷ/֡β
|
// Э<><D0AD>֡ͷ/֡β
|
||||||
|
|
@ -25,7 +26,7 @@ typedef enum
|
||||||
UART4_RCV_BUFF_MIDIEND
|
UART4_RCV_BUFF_MIDIEND
|
||||||
} UART4_RCV_StatusType;
|
} UART4_RCV_StatusType;
|
||||||
|
|
||||||
#define UART4_PROCESS_BUFF_SIZE 32
|
#define UART4_PROCESS_BUFF_SIZE 64 /* 02 04 full chord-map frame is 47 bytes */
|
||||||
|
|
||||||
// UART4<54><34><EFBFBD><EFBFBD>ȫ<EFBFBD>ֱ<EFBFBD><D6B1><EFBFBD>
|
// UART4<54><34><EFBFBD><EFBFBD>ȫ<EFBFBD>ֱ<EFBFBD><D6B1><EFBFBD>
|
||||||
static UART4_RCV_StatusType UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
static UART4_RCV_StatusType UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
||||||
|
|
@ -48,22 +49,25 @@ typedef struct
|
||||||
// ==============================
|
// ==============================
|
||||||
// ָ<><EFBFBD><EEB4A6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȫ<EFBFBD><C8AB> static<69><63>
|
// ָ<><EFBFBD><EEB4A6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȫ<EFBFBD><C8AB> static<69><63>
|
||||||
// ==============================
|
// ==============================
|
||||||
//static void handleDevDisconnect(uint8_t *data);
|
static void handleDevDisconnect(uint8_t *data);
|
||||||
//static void handleDevConnect(uint8_t *data);
|
static void handleDevConnect(uint8_t *data);
|
||||||
//static void handleDevName(uint8_t *data);
|
static void handleDevName(uint8_t *data);
|
||||||
//static void handleFwMainVer(uint8_t *data);
|
static void handleFwMainVer(uint8_t *data);
|
||||||
//static void handleSoundVer(uint8_t *data);
|
static void handleSoundVer(uint8_t *data);
|
||||||
//static void handleUIVer(uint8_t *data);
|
static void handleUIVer(uint8_t *data);
|
||||||
//static void handleOtherInfo(uint8_t *data);
|
static void handleOtherInfo(uint8_t *data);
|
||||||
//static void handleDevCode(uint8_t *data);
|
static void handleDevCode(uint8_t *data);
|
||||||
//static void handleAutoPowerOff(uint8_t *data);
|
static void handleAutoPowerOff(uint8_t *data);
|
||||||
|
static void handleUserFwVer(uint8_t *data);
|
||||||
|
static void handleAutoPowerOffSet(uint8_t *data);
|
||||||
|
static void handleFwCode(uint8_t *data);
|
||||||
|
|
||||||
static void handleReadRhythmMap(uint8_t *data);
|
static void handleReadRhythmMap(uint8_t *data);
|
||||||
static void handlePitchOffset(uint8_t *data);
|
static void handlePitchOffset(uint8_t *data);
|
||||||
static void handleChordOffset(uint8_t *data);
|
static void handleChordOffset(uint8_t *data);
|
||||||
//static void handleResetChordMap(uint8_t *data);
|
static void handleResetChordMap(uint8_t *data);
|
||||||
|
|
||||||
//static void handleRhythmStyle(uint8_t *data);
|
static void handleRhythmStyle(uint8_t *data);
|
||||||
static void handleStringTimbre(uint8_t *data);
|
static void handleStringTimbre(uint8_t *data);
|
||||||
static void handleBPM(uint8_t *data);
|
static void handleBPM(uint8_t *data);
|
||||||
static void handleTranspose(uint8_t *data);
|
static void handleTranspose(uint8_t *data);
|
||||||
|
|
@ -75,9 +79,8 @@ static void handleLED3(uint8_t *data);
|
||||||
static void handleLED4(uint8_t *data);
|
static void handleLED4(uint8_t *data);
|
||||||
static void handleLED5(uint8_t *data);
|
static void handleLED5(uint8_t *data);
|
||||||
static void handleLED6(uint8_t *data);
|
static void handleLED6(uint8_t *data);
|
||||||
//static void handleStopPlay(uint8_t *data);
|
|
||||||
|
|
||||||
//static void handleDeviceReset(uint8_t *data);
|
static void handleDeviceReset(uint8_t *data);
|
||||||
|
|
||||||
static void handleIntro(uint8_t *data);
|
static void handleIntro(uint8_t *data);
|
||||||
static void handleInterlude(uint8_t *data);
|
static void handleInterlude(uint8_t *data);
|
||||||
|
|
@ -98,52 +101,54 @@ static void handleSectionD(uint8_t *data);
|
||||||
*************************************************/
|
*************************************************/
|
||||||
static const BLE_SysExCmdItem bleSysExCmdTable[] =
|
static const BLE_SysExCmdItem bleSysExCmdTable[] =
|
||||||
{
|
{
|
||||||
// //======== 1. <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ 0x01 ========
|
/*======== 1. device info 0x01 ========*/
|
||||||
// {{0x01, 0x00}, handleDevDisconnect}, // <20>Ͽ<EFBFBD><CFBF>豸
|
{{0x01, 0x00}, handleDevDisconnect}, /* disconnect */
|
||||||
// {{0x01, 0x01}, handleDevConnect}, // <20><><EFBFBD><EFBFBD><EFBFBD>豸
|
{{0x01, 0x01}, handleDevConnect}, /* connect */
|
||||||
// {{0x01, 0x02}, handleDevName}, // <20><>ȡ<EFBFBD>豸<EFBFBD><E8B1B8>
|
{{0x01, 0x02}, handleDevName}, /* device name */
|
||||||
// {{0x01, 0x03}, handleFwMainVer}, // <20>̼<EFBFBD><CCBC><EFBFBD><EFBFBD>汾
|
{{0x01, 0x03}, handleFwMainVer}, /* fw main version */
|
||||||
// {{0x01, 0x04}, handleSoundVer}, // <20><>Դ<EFBFBD>汾
|
{{0x01, 0x04}, handleSoundVer}, /* sound version */
|
||||||
// {{0x01, 0x05}, handleUIVer}, // UI<55>汾
|
{{0x01, 0x05}, handleUIVer}, /* UI version */
|
||||||
// {{0x01, 0x06}, handleOtherInfo}, // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
|
{{0x01, 0x06}, handleOtherInfo}, /* other info */
|
||||||
// {{0x01, 0x07}, handleDevCode}, // <20>豸<EFBFBD><E8B1B8><EFBFBD><EFBFBD>
|
{{0x01, 0x07}, handleDevCode}, /* device code (96bit UID) */
|
||||||
// {{0x01, 0x0A}, handleAutoPowerOff}, // <20>Զ<EFBFBD><D4B6>ػ<EFBFBD>
|
{{0x01, 0x0A}, handleAutoPowerOff}, /* auto power-off read (minutes) */
|
||||||
//
|
{{0x01, 0x0C}, handleUserFwVer}, /* user fw version */
|
||||||
// //======== 2. ӳ<><D3B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 0x02 ========
|
{{0x01, 0x11}, handleAutoPowerOffSet}, /* auto power-off set (minutes) */
|
||||||
{{0x02, 0x01}, handleReadRhythmMap}, // <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD>/<2F><><EFBFBD><EFBFBD>ӳ<EFBFBD><D3B3><EFBFBD>
|
{{0x01, 0x0F}, handleFwCode}, /* fw code (MIDI/BLE-safe; replaces 01 FF) */
|
||||||
{{0x02, 0x02}, handlePitchOffset}, // Pitchƫ<68><C6AB>
|
{{0x01, 0xFF}, handleFwCode}, /* fw code alias (raw UART only; 0xFF illegal in BLE-MIDI SysEx) */
|
||||||
{{0x02, 0x03}, handleChordOffset}, // Chordƫ<64><C6AB>
|
|
||||||
// {{0x02, 0x04}, handleResetChordMap}, // <20><><EFBFBD>ú<EFBFBD><C3BA><EFBFBD>ӳ<EFBFBD><D3B3>Ĭ<EFBFBD><C4AC>ֵ
|
|
||||||
|
|
||||||
//======== 3. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>д 0x03 ========
|
/*======== 2. chord map 0x02 ========*/
|
||||||
// {{0x03, 0x04}, handleRhythmStyle}, // <20><>ȡ/<2F><><EFBFBD>ý<EFBFBD><C3BD><EFBFBD><EFBFBD><EFBFBD>
|
{{0x02, 0x01}, handleReadRhythmMap}, /* read chord/pitch map */
|
||||||
{{0x03, 0x05}, handleStringTimbre}, // <20><>ȡ/<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɫ
|
{{0x02, 0x02}, handlePitchOffset}, /* pitch offset */
|
||||||
{{0x03, 0x06}, handleBPM}, // <20><>ȡ/<2F><><EFBFBD><EFBFBD>BPM
|
{{0x02, 0x03}, handleChordOffset}, /* chord offset */
|
||||||
{{0x03, 0x07}, handleTranspose}, // <20><>ȡ/<2F><><EFBFBD><EFBFBD><EFBFBD>Ƶ<EFBFBD>
|
{{0x02, 0x04}, handleResetChordMap}, /* write whole map (default reset) */
|
||||||
|
|
||||||
//======== 4. <20><><EFBFBD><EFBFBD>/LED<45><44><EFBFBD><EFBFBD> 0x04 ========
|
/*======== 3. guitar params 0x03 ========*/
|
||||||
{{0x04, 0x00}, handleLED0}, // <20><>1<EFBFBD><31>LED
|
{{0x03, 0x04}, handleRhythmStyle}, /* rhythm style r/w + user list */
|
||||||
{{0x04, 0x01}, handleLED1}, // <20><>2<EFBFBD><32>LED
|
{{0x03, 0x05}, handleStringTimbre}, /* string timbre r/w */
|
||||||
{{0x04, 0x02}, handleLED2}, // <20><>3<EFBFBD><33>LED
|
{{0x03, 0x06}, handleBPM}, /* BPM r/w */
|
||||||
{{0x04, 0x03}, handleLED3}, // <20><>4<EFBFBD><34>LED
|
{{0x03, 0x07}, handleTranspose}, /* transpose r/w */
|
||||||
{{0x04, 0x04}, handleLED4}, // <20><>5<EFBFBD><35>LED
|
|
||||||
{{0x04, 0x05}, handleLED5}, // <20><>6<EFBFBD><36>LED
|
|
||||||
{{0x04, 0x06}, handleLED6}, // <20><>7<EFBFBD><37>LED
|
|
||||||
// {{0x04, 0x07}, handleStopPlay}, // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
|
||||||
|
|
||||||
// //======== 5. <20><>λ/<2F>ػ<EFBFBD> 0x05 ========
|
/*======== 4. play / LED 0x04 ========*/
|
||||||
// {{0x05, 0x00}, handleDeviceReset}, // <20>豸<EFBFBD><E8B1B8>λ
|
{{0x04, 0x00}, handleLED0}, /* LED 1 */
|
||||||
//
|
{{0x04, 0x01}, handleLED1}, /* LED 2 */
|
||||||
// //======== 6. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת 0x06 ========
|
{{0x04, 0x02}, handleLED2}, /* LED 3 */
|
||||||
{{0x06, 0x01}, handleIntro}, // ǰ<><C7B0>
|
{{0x04, 0x03}, handleLED3}, /* LED 4 */
|
||||||
{{0x06, 0x02}, handleInterlude}, // <20><><EFBFBD><EFBFBD>
|
{{0x04, 0x04}, handleLED4}, /* LED 5 */
|
||||||
{{0x06, 0x03}, handleOutro}, // β<><CEB2>
|
{{0x04, 0x05}, handleLED5}, /* LED 6 */
|
||||||
{{0x04, 0x07}, handleEnd}, // end
|
{{0x04, 0x06}, handleLED6}, /* LED 7 */
|
||||||
{{0x06, 0x05}, handleSectionA}, // A<><41>
|
{{0x04, 0x07}, handleEnd}, /* end / stop play */
|
||||||
{{0x06, 0x06}, handleSectionB}, // B<><42>
|
|
||||||
{{0x06, 0x07}, handleSectionC}, // C<><43>
|
|
||||||
{{0x06, 0x08}, handleSectionD}, // D<><44>
|
|
||||||
|
|
||||||
|
/*======== 5. reset / power off 0x05 ========*/
|
||||||
|
{{0x05, 0x00}, handleDeviceReset}, /* device reset -> soft power off */
|
||||||
|
|
||||||
|
/*======== 6. section jump 0x06 ========*/
|
||||||
|
{{0x06, 0x01}, handleIntro}, /* intro */
|
||||||
|
{{0x06, 0x02}, handleInterlude}, /* interlude */
|
||||||
|
{{0x06, 0x03}, handleOutro}, /* outro */
|
||||||
|
{{0x06, 0x05}, handleSectionA}, /* section A */
|
||||||
|
{{0x06, 0x06}, handleSectionB}, /* section B */
|
||||||
|
{{0x06, 0x07}, handleSectionC}, /* section C */
|
||||||
|
{{0x06, 0x08}, handleSectionD}, /* section D */
|
||||||
};
|
};
|
||||||
|
|
||||||
// ָ<><D6B8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
// ָ<><D6B8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
|
@ -158,14 +163,16 @@ static const BLE_SysExCmdItem bleSysExCmdTable[] =
|
||||||
*/
|
*/
|
||||||
void processBLESysEXData(uint8_t* data, uint8_t cnt)
|
void processBLESysEXData(uint8_t* data, uint8_t cnt)
|
||||||
{
|
{
|
||||||
//ResetAutoPowerCount();
|
// ֡У<D6A1><D0A3>
|
||||||
// <20><><EFBFBD><EFBFBD>֡У<D6A1><D0A3>
|
|
||||||
if (data[0] != FRAME_SYS_HEAD || data[cnt - 1] != FRAME_SYS_TAIL || cnt > UART4_PROCESS_BUFF_SIZE || data[1] != 0x60)
|
if (data[0] != FRAME_SYS_HEAD || data[cnt - 1] != FRAME_SYS_TAIL || cnt > UART4_PROCESS_BUFF_SIZE || data[1] != 0x60)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
/* App/BLE 活动视为用户操作,避免测协议时静置触发自动关机 */
|
||||||
|
ResetAutoPowerCount();
|
||||||
|
LOG_I("BLE", "sysex ok len=%u cmd=%02X %02X", (unsigned)cnt, data[2], data[3]);
|
||||||
|
|
||||||
// <20><>ȡ <20><>ָ<EFBFBD><D6B8> + <20><>ָ<EFBFBD><D6B8>
|
// ȡ<EFBFBD><EFBFBD>ָ<EFBFBD><EFBFBD> + <20><>ָ<EFBFBD><D6B8>
|
||||||
uint8_t cmd[2] = {data[2], data[3]};
|
uint8_t cmd[2] = {data[2], data[3]};
|
||||||
|
|
||||||
// <20><><EFBFBD><EFBFBD>ָ<EFBFBD><D6B8><EFBFBD>ƥ<EFBFBD><C6A5>
|
// <20><><EFBFBD><EFBFBD>ָ<EFBFBD><D6B8><EFBFBD>ƥ<EFBFBD><C6A5>
|
||||||
|
|
@ -195,6 +202,7 @@ void UART4_Data_Process(volatile uint8_t* data)
|
||||||
last_tick = now;
|
last_tick = now;
|
||||||
memset(UART4_Process_Buff, 0, sizeof(UART4_Process_Buff));
|
memset(UART4_Process_Buff, 0, sizeof(UART4_Process_Buff));
|
||||||
UART4_RCV_cnt = 0;
|
UART4_RCV_cnt = 0;
|
||||||
|
UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (UART4_RCV_Status)
|
switch (UART4_RCV_Status)
|
||||||
|
|
@ -251,7 +259,6 @@ void UART4_Data_Process(volatile uint8_t* data)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// <20>Ƿ<EFBFBD><C7B7>ֽڣ<D6BD><DAA3><EFBFBD>λ
|
|
||||||
UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -346,8 +353,9 @@ static void handleTranspose(uint8_t *data)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
{
|
{
|
||||||
uint8_t ReturnTranspose[8] = {0xF0,0x60,0x03,0x07,0x00,0x00,0x00,0xF7};
|
/* doc: F0 60 03 07 00 <val> F7 (7 bytes total) */
|
||||||
ReturnTranspose[5] = mGuiData[GUI_TRANSPOSE].Current;
|
uint8_t ReturnTranspose[7] = {0xF0,0x60,0x03,0x07,0x00,0x00,0xF7};
|
||||||
|
ReturnTranspose[5] = (uint8_t)mGuiData[GUI_TRANSPOSE].Current;
|
||||||
USART4_SendData(ReturnTranspose,sizeof(ReturnTranspose));
|
USART4_SendData(ReturnTranspose,sizeof(ReturnTranspose));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -472,25 +480,42 @@ static void handleLED6(uint8_t *data)
|
||||||
|
|
||||||
static void handlePitchOffset(uint8_t *data)
|
static void handlePitchOffset(uint8_t *data)
|
||||||
{
|
{
|
||||||
BL_Sendmsg(MSG_ID_PITCH_OFFSET, data[4]+1, data[5], 0);
|
uint8_t key = (uint8_t)(data[4] + 1u);
|
||||||
|
if (key < 1u || key > 21u)
|
||||||
|
return;
|
||||||
|
BL_Sendmsg(MSG_ID_PITCH_OFFSET, key, data[5], 0);
|
||||||
}
|
}
|
||||||
static void handleChordOffset(uint8_t *data)
|
static void handleChordOffset(uint8_t *data)
|
||||||
{
|
{
|
||||||
BL_Sendmsg(MSG_ID_CHORD_OFFSET, data[4]+1, data[5], 0);
|
uint8_t key = (uint8_t)(data[4] + 1u);
|
||||||
|
if (key < 1u || key > 21u)
|
||||||
|
return;
|
||||||
|
BL_Sendmsg(MSG_ID_CHORD_OFFSET, key, data[5], 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
extern CHORD_TYPE_INDEX chord_type_index_map[22];
|
extern CHORD_TYPE_INDEX chord_type_index_map[22];
|
||||||
static void handleReadRhythmMap(uint8_t *data)
|
static void handleReadRhythmMap(uint8_t *data)
|
||||||
{
|
{
|
||||||
uint8_t ReadChordMap[47] = {0xF0, 0x60, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7};
|
/* 静态缓冲:禁止在 TaskBTRecv 小栈上再开 47B */
|
||||||
|
static uint8_t ReadChordMap[47];
|
||||||
uint8_t chord_map = 0;
|
uint8_t chord_map = 0;
|
||||||
for(uint8_t i = 1;i < 22;i ++)
|
uint8_t i;
|
||||||
|
|
||||||
|
(void)data;
|
||||||
|
memset(ReadChordMap, 0, sizeof(ReadChordMap));
|
||||||
|
ReadChordMap[0] = 0xF0;
|
||||||
|
ReadChordMap[1] = 0x60;
|
||||||
|
ReadChordMap[2] = 0x02;
|
||||||
|
ReadChordMap[3] = 0x01;
|
||||||
|
ReadChordMap[46] = 0xF7;
|
||||||
|
|
||||||
|
for (i = 1; i < 22; i++)
|
||||||
{
|
{
|
||||||
ReadChordMap[3+i] = chord_type_index_map[i].PitchOffset;
|
ReadChordMap[3 + i] = chord_type_index_map[i].PitchOffset;
|
||||||
}
|
}
|
||||||
for(uint8_t i = 25;i < 46;i ++)
|
for (i = 25; i < 46; i++)
|
||||||
{
|
{
|
||||||
switch(chord_type_index_map[i-24].type)
|
switch (chord_type_index_map[i - 24].type)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
chord_map = 0;
|
chord_map = 0;
|
||||||
|
|
@ -519,10 +544,13 @@ static void handleReadRhythmMap(uint8_t *data)
|
||||||
case 8:
|
case 8:
|
||||||
chord_map = 11;
|
chord_map = 11;
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
chord_map = 0;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
ReadChordMap[i] = chord_map;
|
ReadChordMap[i] = chord_map;
|
||||||
}
|
}
|
||||||
USART4_SendData(ReadChordMap,sizeof(ReadChordMap));
|
USART4_SendData(ReadChordMap, sizeof(ReadChordMap));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handleIntro(uint8_t *data)
|
static void handleIntro(uint8_t *data)
|
||||||
|
|
@ -546,7 +574,7 @@ static void handleEnd(uint8_t *data)
|
||||||
// TM1629D_AllLedOn(LED_COLOR_G);
|
// TM1629D_AllLedOn(LED_COLOR_G);
|
||||||
// TM1629D_UpdateDisplay(0);
|
// TM1629D_UpdateDisplay(0);
|
||||||
// osMutexRelease(Tm1629Mutex);
|
// osMutexRelease(Tm1629Mutex);
|
||||||
BL_Sendmsg(MSG_ID_ADCIN1KEY, 1, 0, 0);
|
BL_Sendmsg(MSG_ID_ADCIN1KEY, 2, 0, 0); /* End -> Postamble(1), distinct from Outro(key=1) */
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handleSectionA(uint8_t *data)
|
static void handleSectionA(uint8_t *data)
|
||||||
|
|
@ -570,3 +598,270 @@ static void BL_Sendmsg(uint16_t Data1, uint16_t Data2, uint16_t Data3, uint16_t
|
||||||
{
|
{
|
||||||
BLTask_Sendmsg(Data1, Data2, Data3, Data4);
|
BLTask_Sendmsg(Data1, Data2, Data3, Data4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================================================================
|
||||||
|
* Appended protocol handlers (per Doc/指令测试.docx)
|
||||||
|
* ==================================================================== */
|
||||||
|
|
||||||
|
/*-------- 1. device info 0x01 --------*/
|
||||||
|
#define AT32_UID_BASE 0x1FFFF7E8UL /* 96-bit unique device ID */
|
||||||
|
|
||||||
|
/* 01 04 音源版本:YY.M.D = 音色资源日期(Dream 无版本查询,发版时手改)
|
||||||
|
* 26.9.8 → 2026-09-08(Doc/音色文件/0908) */
|
||||||
|
static const uint8_t SOUND_VER[4] = {26, 9, 8, 0x01};
|
||||||
|
/* 01 05 UI 资源/界面日期:YY.M.D(与 MCU 逻辑版本 Version[] 分开维护) */
|
||||||
|
static const uint8_t UI_VER[4] = {26, 9, 9, 0x01};
|
||||||
|
|
||||||
|
static void handleDevDisconnect(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t resp[6] = {0xF0, 0x60, 0x01, 0x00, 0x01, 0xF7};
|
||||||
|
(void)data;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleDevConnect(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t resp[6] = {0xF0, 0x60, 0x01, 0x01, 0x01, 0xF7};
|
||||||
|
(void)data;
|
||||||
|
ResetAutoPowerCount();
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleDevName(uint8_t *data)
|
||||||
|
{
|
||||||
|
static char name[BLE_DEVICE_NAME_LEN + 1];
|
||||||
|
static uint8_t resp[4 + BLE_DEVICE_NAME_LEN + 1]; /* head4 + name + F7 */
|
||||||
|
(void)data;
|
||||||
|
BleBuildDeviceName(name);
|
||||||
|
resp[0] = 0xF0; resp[1] = 0x60; resp[2] = 0x01; resp[3] = 0x02;
|
||||||
|
memcpy(&resp[4], name, BLE_DEVICE_NAME_LEN);
|
||||||
|
resp[4 + BLE_DEVICE_NAME_LEN] = 0xF7;
|
||||||
|
USART4_SendData(resp, (uint16_t)sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleFwMainVer(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t resp[9] = {0xF0, 0x60, 0x01, 0x03, Version[0], Version[1], Version[2], 0x01, 0xF7};
|
||||||
|
(void)data;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleSoundVer(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t resp[9] = {0xF0, 0x60, 0x01, 0x04, SOUND_VER[0], SOUND_VER[1], SOUND_VER[2], SOUND_VER[3], 0xF7};
|
||||||
|
(void)data;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleUIVer(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t resp[9] = {0xF0, 0x60, 0x01, 0x05, UI_VER[0], UI_VER[1], UI_VER[2], UI_VER[3], 0xF7};
|
||||||
|
(void)data;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleOtherInfo(uint8_t *data)
|
||||||
|
{
|
||||||
|
/* 01 06: reserved — keep 8 zero bytes until product defines fields */
|
||||||
|
uint8_t resp[13] = {0xF0, 0x60, 0x01, 0x06, 0, 0, 0, 0, 0, 0, 0, 0, 0xF7};
|
||||||
|
(void)data;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleDevCode(uint8_t *data)
|
||||||
|
{
|
||||||
|
/* 96-bit UID as 24 hex ASCII — SysEx data must stay 7-bit for BLE-MIDI */
|
||||||
|
static const char hex[] = "0123456789ABCDEF";
|
||||||
|
uint8_t resp[29];
|
||||||
|
const uint8_t *uid = (const uint8_t *)AT32_UID_BASE;
|
||||||
|
uint8_t i;
|
||||||
|
(void)data;
|
||||||
|
resp[0] = 0xF0;
|
||||||
|
resp[1] = 0x60;
|
||||||
|
resp[2] = 0x01;
|
||||||
|
resp[3] = 0x07;
|
||||||
|
for (i = 0; i < 12u; i++)
|
||||||
|
{
|
||||||
|
resp[4u + 2u * i] = (uint8_t)hex[uid[i] >> 4];
|
||||||
|
resp[4u + 2u * i + 1u] = (uint8_t)hex[uid[i] & 0x0Fu];
|
||||||
|
}
|
||||||
|
resp[28] = 0xF7;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleAutoPowerOff(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t resp[6] = {0xF0, 0x60, 0x01, 0x0A, 0x00, 0xF7};
|
||||||
|
(void)data;
|
||||||
|
resp[4] = AutoCloseTime;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleUserFwVer(uint8_t *data)
|
||||||
|
{
|
||||||
|
/* 用户固件版本 = "{branch}_{short6}"[+ '*'],如 develop_0aedb4(ASCII,BLE-MIDI 安全) */
|
||||||
|
uint8_t resp[4u + GIT_BUILD_ID_LEN + 1u];
|
||||||
|
(void)data;
|
||||||
|
resp[0] = 0xF0;
|
||||||
|
resp[1] = 0x60;
|
||||||
|
resp[2] = 0x01;
|
||||||
|
resp[3] = 0x0C;
|
||||||
|
memcpy(&resp[4], GIT_BUILD_ID, GIT_BUILD_ID_LEN);
|
||||||
|
resp[4u + GIT_BUILD_ID_LEN] = 0xF7;
|
||||||
|
USART4_SendData(resp, (uint16_t)sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* F0 60 01 11 <minutes> F7 : set auto power-off, 0 = disable */
|
||||||
|
static void handleAutoPowerOffSet(uint8_t *data)
|
||||||
|
{
|
||||||
|
NvmParam_Type *nvm = drv_nvm_param_ptr();
|
||||||
|
uint8_t resp[6] = {0xF0, 0x60, 0x01, 0x11, 0x00, 0xF7};
|
||||||
|
AutoCloseTime = data[4];
|
||||||
|
if (nvm->param.AutoCloseTime != AutoCloseTime)
|
||||||
|
{
|
||||||
|
nvm->param.AutoCloseTime = AutoCloseTime;
|
||||||
|
drv_nvm_save_to_flash();
|
||||||
|
}
|
||||||
|
ResetAutoPowerCount();
|
||||||
|
resp[4] = AutoCloseTime;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleFwCode(uint8_t *data)
|
||||||
|
{
|
||||||
|
/* 固件编码: 请求 01 0F(推荐)或 01 FF(仅裸串口);应答统一 01 0F + ASCII,避免 BLE-MIDI 吃掉 0xFF */
|
||||||
|
uint8_t resp[16];
|
||||||
|
uint8_t len = (uint8_t)strlen(fwname);
|
||||||
|
(void)data;
|
||||||
|
if (len > 10u)
|
||||||
|
len = 10u;
|
||||||
|
resp[0] = 0xF0;
|
||||||
|
resp[1] = 0x60;
|
||||||
|
resp[2] = 0x01;
|
||||||
|
resp[3] = 0x0F;
|
||||||
|
memcpy(&resp[4], fwname, len);
|
||||||
|
resp[4 + len] = 0xF7;
|
||||||
|
USART4_SendData(resp, (uint16_t)(4u + len + 1u));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*-------- 2. chord map 0x02 --------*/
|
||||||
|
/* F0 60 02 04 <21B pitch> <21B chord> F7 : write whole map; reply in 02 01 format */
|
||||||
|
static void handleResetChordMap(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t key;
|
||||||
|
for (key = 1; key <= 21; key++)
|
||||||
|
{
|
||||||
|
BT_Pitch_offset_map(key, data[3 + key]); /* data[4..24] pitch offsets */
|
||||||
|
BT_Chord_offset_map(key, data[24 + key]); /* data[25..45] chord types */
|
||||||
|
}
|
||||||
|
handleReadRhythmMap(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*-------- 3. rhythm style 0x03 0x04 --------*/
|
||||||
|
static void handleRhythmStyle(uint8_t *data)
|
||||||
|
{
|
||||||
|
switch (data[4])
|
||||||
|
{
|
||||||
|
case 0: /* read current style: F0 60 03 04 00 <src> <idHi> <idLo> F7 */
|
||||||
|
{
|
||||||
|
uint8_t resp[9] = {0xF0, 0x60, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0xF7};
|
||||||
|
uint8_t src = (uint8_t)(mGuiData[GUI_AUTOBAND_SW].Current ? 1 : 0);
|
||||||
|
uint16_t id = src ? ParamGuiData[EXPRESS_MODE_PARAM].Current
|
||||||
|
: ParamGuiData[SONG_MODE_PARAM].Current;
|
||||||
|
resp[5] = src;
|
||||||
|
resp[6] = (uint8_t)(id / 128);
|
||||||
|
resp[7] = (uint8_t)(id % 128);
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 1: /* set style: F0 60 03 04 01 <src:0 sys/1 user> <idHi> <idLo> F7, reply BPM */
|
||||||
|
{
|
||||||
|
uint8_t src = (data[5] != 0) ? 1 : 0;
|
||||||
|
uint16_t id = (uint16_t)data[6] * 128 + data[7];
|
||||||
|
GUI_SWITCH *slot = src ? &ParamGuiData[EXPRESS_MODE_PARAM]
|
||||||
|
: &ParamGuiData[SONG_MODE_PARAM];
|
||||||
|
uint8_t resp[7] = {0xF0, 0x60, 0x03, 0x04, 0x01, 0x00, 0xF7};
|
||||||
|
if (src)
|
||||||
|
slot->Max = (LOCAL_SONG_COUNT > 0) ? (uint16_t)(LOCAL_SONG_COUNT - 1) : 0;
|
||||||
|
if (id > slot->Max)
|
||||||
|
id = slot->Max;
|
||||||
|
mGuiData[GUI_AUTOBAND_SW].Current = src;
|
||||||
|
slot->Current = id;
|
||||||
|
StartFlag = 0;
|
||||||
|
AutoBandTop1_Stop();
|
||||||
|
UI_ReloadTonePreset();
|
||||||
|
/* doc: single-byte bpm; clamp >127 (App should read exact bpm via 03 06) */
|
||||||
|
resp[5] = (mGuiData[GUI_SPEED].Current > 0x7F)
|
||||||
|
? 0x7F : (uint8_t)mGuiData[GUI_SPEED].Current;
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
|
||||||
|
/* boot / song-entry special: F0 60 03 04 01 00 00 00 F7
|
||||||
|
doc: also push current BPM as active report F0 51 03 <hi> <lo> F7 */
|
||||||
|
if (data[5] == 0 && data[6] == 0 && data[7] == 0)
|
||||||
|
{
|
||||||
|
uint8_t bpmr[6] = {0xF0, 0x51, 0x03, 0x00, 0x00, 0xF7};
|
||||||
|
bpmr[3] = (uint8_t)(mGuiData[GUI_SPEED].Current / 128);
|
||||||
|
bpmr[4] = (uint8_t)(mGuiData[GUI_SPEED].Current % 128);
|
||||||
|
USART4_SendData(bpmr, sizeof(bpmr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2: /* user style list page: F0 60 03 04 02 <from3B> <to3B> F7
|
||||||
|
reply: F0 60 03 04 02 <cntHi> <cntLo> [<idHi> <idLo> <code 4B ascii> ...] F7 */
|
||||||
|
{
|
||||||
|
uint8_t resp[64];
|
||||||
|
uint32_t from = 0, to = LOCAL_SONG_COUNT;
|
||||||
|
uint16_t i, n = 0, len;
|
||||||
|
/* from/to: 24-bit big-endian, optional (doc example: 00 00 00 07) */
|
||||||
|
if (data[5] || data[6] || data[7])
|
||||||
|
{
|
||||||
|
from = ((uint32_t)data[5] << 16) | ((uint32_t)data[6] << 8) | data[7];
|
||||||
|
to = ((uint32_t)data[8] << 16) | ((uint32_t)data[9] << 8) | data[10];
|
||||||
|
}
|
||||||
|
if (from > LOCAL_SONG_COUNT) from = LOCAL_SONG_COUNT;
|
||||||
|
if (to > LOCAL_SONG_COUNT || to <= from) to = LOCAL_SONG_COUNT;
|
||||||
|
resp[0] = 0xF0; resp[1] = 0x60; resp[2] = 0x03; resp[3] = 0x04; resp[4] = 0x02;
|
||||||
|
len = 7; /* count filled after loop */
|
||||||
|
for (i = (uint16_t)from; i < to; i++)
|
||||||
|
{
|
||||||
|
if (len + 6 + 1 > sizeof(resp)) break;
|
||||||
|
resp[len++] = (uint8_t)(i / 128);
|
||||||
|
resp[len++] = (uint8_t)(i % 128);
|
||||||
|
memcpy(&resp[len], LocalSongCode[i], 4);
|
||||||
|
len += 4;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
resp[5] = (uint8_t)(n / 128);
|
||||||
|
resp[6] = (uint8_t)(n % 128);
|
||||||
|
resp[len++] = 0xF7;
|
||||||
|
USART4_SendData(resp, len);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3: /* user style total: reply F0 60 03 04 00 <total 3B BE> F7 (doc: 00 00 1E) */
|
||||||
|
{
|
||||||
|
uint8_t resp[9] = {0xF0, 0x60, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0xF7};
|
||||||
|
resp[5] = (uint8_t)((LOCAL_SONG_COUNT >> 16) & 0xFF);
|
||||||
|
resp[6] = (uint8_t)((LOCAL_SONG_COUNT >> 8) & 0xFF);
|
||||||
|
resp[7] = (uint8_t)(LOCAL_SONG_COUNT & 0xFF);
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*-------- 5. reset / power off 0x05 --------*/
|
||||||
|
/* F0 60 05 00 F7 : ack then soft power off (executed in main-loop context) */
|
||||||
|
static void handleDeviceReset(uint8_t *data)
|
||||||
|
{
|
||||||
|
uint8_t resp[6] = {0xF0, 0x60, 0x05, 0x00, 0x01, 0xF7};
|
||||||
|
(void)data;
|
||||||
|
LOG_I("BLE", "05 00 ack then soft power off");
|
||||||
|
USART4_SendData(resp, sizeof(resp));
|
||||||
|
/* Soft-off cuts BT power immediately in PowerOff(); give UART4 + ATS2853
|
||||||
|
time to push the ACK Notify before requesting power-off. */
|
||||||
|
USART4_WaitTxIdle(50);
|
||||||
|
rt_thread_mdelay(120);
|
||||||
|
System_RequestPowerOff();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/* Auto-generated by tools/gen_git_user_fw_ver.py — do not edit.
|
||||||
|
* git HEAD (dirty): 30c4ea9c0de352263e5ca24547672b39c9d86e35
|
||||||
|
* branch: develop → develop
|
||||||
|
* 01 0C wire: develop_30c4ea*
|
||||||
|
*/
|
||||||
|
#ifndef GIT_USER_FW_VER_H
|
||||||
|
#define GIT_USER_FW_VER_H
|
||||||
|
|
||||||
|
#define GIT_COMMIT_ID_FULL "30c4ea9c0de352263e5ca24547672b39c9d86e35"
|
||||||
|
#define GIT_BRANCH_NAME "develop"
|
||||||
|
#define GIT_COMMIT_SHORT6 "30c4ea"
|
||||||
|
#define GIT_DIRTY (1)
|
||||||
|
#define GIT_BUILD_ID "develop_30c4ea*"
|
||||||
|
#define GIT_BUILD_ID_LEN 15u
|
||||||
|
|
||||||
|
#endif /* GIT_USER_FW_VER_H */
|
||||||
|
|
@ -13,7 +13,7 @@ ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskUIThread_Stack[2048];
|
||||||
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskTouchThread_Stack[1536];
|
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskTouchThread_Stack[1536];
|
||||||
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskMainThread_Stack[512];
|
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskMainThread_Stack[512];
|
||||||
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskBTTHandlehread_Stack[512];
|
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskBTTHandlehread_Stack[512];
|
||||||
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskBTRecvThread_Stack[512];
|
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskBTRecvThread_Stack[768];
|
||||||
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskAutobandThread_Stack[512];
|
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskAutobandThread_Stack[512];
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -115,6 +115,9 @@ void TaskBTHandleThread_entry(void* parameter)
|
||||||
case MSG_ID_LIGHT_LED : BL_Set_led(xEvent.ID,xEvent.HiByte); break;
|
case MSG_ID_LIGHT_LED : BL_Set_led(xEvent.ID,xEvent.HiByte); break;
|
||||||
case MSG_ID_PITCH_OFFSET : BT_Pitch_offset_map(xEvent.ID,xEvent.HiByte); break;
|
case MSG_ID_PITCH_OFFSET : BT_Pitch_offset_map(xEvent.ID,xEvent.HiByte); break;
|
||||||
case MSG_ID_CHORD_OFFSET : BT_Chord_offset_map(xEvent.ID,xEvent.HiByte); break;
|
case MSG_ID_CHORD_OFFSET : BT_Chord_offset_map(xEvent.ID,xEvent.HiByte); break;
|
||||||
|
/* BLE section / intro-outro-end commands (06 xx, 04 07) */
|
||||||
|
case MSG_ID_ADCIN1KEY : ADC_IN1_KEY_Handle((uint8_t)xEvent.ID, true); break;
|
||||||
|
case MSG_ID_KEY_1617 : TM1617_Handle((uint8_t)xEvent.ID); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -125,11 +128,10 @@ void TaskBTRecvThread_entry(void* parameter)
|
||||||
uint8_t c;
|
uint8_t c;
|
||||||
while(1)
|
while(1)
|
||||||
{
|
{
|
||||||
if(rt_sem_take(UART4_sem,RT_WAITING_FOREVER) == RT_EOK)
|
if(rt_sem_take(UART4_sem, RT_WAITING_FOREVER) == RT_EOK)
|
||||||
{
|
{
|
||||||
while(UART4_RxPop(&c))
|
while(UART4_RxPop(&c))
|
||||||
{
|
{
|
||||||
//解析函数
|
|
||||||
UART4_Data_Process(&c);
|
UART4_Data_Process(&c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -238,6 +240,35 @@ void StartTask(void)
|
||||||
/* 扫描任务可能已由 StartScanTask 拉起;重复 startup 会被 RT-Thread 忽略 */
|
/* 扫描任务可能已由 StartScanTask 拉起;重复 startup 会被 RT-Thread 忽略 */
|
||||||
StartScanTask();
|
StartScanTask();
|
||||||
|
|
||||||
|
/* Soft-off(StopFullTask) 会 detach 下列线程;开机须能重建,否则 UI/消息永久失效 */
|
||||||
|
if ((TaskMainThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
|
||||||
|
{
|
||||||
|
rt_thread_init(&TaskMainThread,
|
||||||
|
"TaskMainThread",
|
||||||
|
TaskMainThread_entry,
|
||||||
|
RT_NULL,
|
||||||
|
&TaskMainThread_Stack,
|
||||||
|
sizeof(TaskMainThread_Stack),
|
||||||
|
5,
|
||||||
|
20);
|
||||||
|
}
|
||||||
|
if ((TaskMainThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_INIT)
|
||||||
|
rt_thread_startup(&TaskMainThread);
|
||||||
|
|
||||||
|
if ((TaskUIThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
|
||||||
|
{
|
||||||
|
rt_thread_init(&TaskUIThread,
|
||||||
|
"TaskUIThread",
|
||||||
|
TaskUIThread_entry,
|
||||||
|
RT_NULL,
|
||||||
|
&TaskUIThread_Stack,
|
||||||
|
sizeof(TaskUIThread_Stack),
|
||||||
|
5,
|
||||||
|
20);
|
||||||
|
}
|
||||||
|
if ((TaskUIThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_INIT)
|
||||||
|
rt_thread_startup(&TaskUIThread);
|
||||||
|
|
||||||
if ((TaskBTHandleThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
|
if ((TaskBTHandleThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
|
||||||
{
|
{
|
||||||
rt_thread_init(&TaskBTHandleThread,
|
rt_thread_init(&TaskBTHandleThread,
|
||||||
|
|
@ -334,18 +365,12 @@ void StopFullTask(void)
|
||||||
LOG_I("TASK", "stop ScanThread");
|
LOG_I("TASK", "stop ScanThread");
|
||||||
rt_thread_detach(&TaskScanThread);
|
rt_thread_detach(&TaskScanThread);
|
||||||
}
|
}
|
||||||
if((TaskUIThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
/* 软关机 = 系统关闭:停触摸/扫描/BT/伴奏。
|
||||||
LOG_I("TASK", "stop UIThread");
|
Main/UI 保留:Type-C 时要画 Charging Image,长按开机要收 POWER_ON。 */
|
||||||
rt_thread_detach(&TaskUIThread);
|
|
||||||
}
|
|
||||||
if((TaskTouchThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
if((TaskTouchThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
||||||
LOG_I("TASK", "stop TouchThread");
|
LOG_I("TASK", "stop TouchThread");
|
||||||
rt_thread_detach(&TaskTouchThread);
|
rt_thread_detach(&TaskTouchThread);
|
||||||
}
|
}
|
||||||
if((TaskMainThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
|
||||||
LOG_I("TASK", "stop MainThread");
|
|
||||||
rt_thread_detach(&TaskMainThread);
|
|
||||||
}
|
|
||||||
if((TaskBTHandleThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
if((TaskBTHandleThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
||||||
LOG_I("TASK", "stop BTHandleThread");
|
LOG_I("TASK", "stop BTHandleThread");
|
||||||
rt_thread_detach(&TaskBTHandleThread);
|
rt_thread_detach(&TaskBTHandleThread);
|
||||||
|
|
@ -354,6 +379,10 @@ void StopFullTask(void)
|
||||||
LOG_I("TASK", "stop BTRecvThread");
|
LOG_I("TASK", "stop BTRecvThread");
|
||||||
rt_thread_detach(&TaskBTRecvThread);
|
rt_thread_detach(&TaskBTRecvThread);
|
||||||
}
|
}
|
||||||
|
if((TaskAutobandThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
|
||||||
|
LOG_I("TASK", "stop AutobandThread");
|
||||||
|
rt_thread_detach(&TaskAutobandThread);
|
||||||
|
}
|
||||||
LOG_I("TASK", "StopFullTask done");
|
LOG_I("TASK", "StopFullTask done");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,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())
|
||||||
|
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,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,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,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())
|
||||||
|
|
@ -61,8 +61,14 @@ def rgba_to_rgb565(im):
|
||||||
|
|
||||||
|
|
||||||
def rgba_to_rgb565_opaque(im):
|
def rgba_to_rgb565_opaque(im):
|
||||||
"""Full-screen boot/charge: keep dark background pixels (no near-black punch-through)."""
|
"""Full-screen boot/charge/flash: keep dark bg; flatten RGBA onto black (not white).
|
||||||
im = im.convert("RGB")
|
|
||||||
|
Design exports like 烧录模式.png are mostly transparent with dark glyphs; a bare
|
||||||
|
convert('RGB') drops alpha and turns transparent into white — wrong on device.
|
||||||
|
"""
|
||||||
|
rgba = im.convert("RGBA")
|
||||||
|
bg = Image.new("RGBA", rgba.size, (0, 0, 0, 255))
|
||||||
|
im = Image.alpha_composite(bg, rgba).convert("RGB")
|
||||||
px = im.load()
|
px = im.load()
|
||||||
w, h = im.size
|
w, h = im.size
|
||||||
out = bytearray(w * h * 2)
|
out = bytearray(w * h * 2)
|
||||||
|
|
@ -131,14 +137,14 @@ INTERNAL = [
|
||||||
("gImage_UI0902_Volume_20x15", "02_顶部图标/音量图标-10.png"),
|
("gImage_UI0902_Volume_20x15", "02_顶部图标/音量图标-10.png"),
|
||||||
("gImage_UI0902_Bluetooth_11x17", "02_顶部图标/蓝牙图标-10.png"),
|
("gImage_UI0902_Bluetooth_11x17", "02_顶部图标/蓝牙图标-10.png"),
|
||||||
# BatteryFill generated by make_battery_fill() — not from mockup PNG
|
# BatteryFill generated by make_battery_fill() — not from mockup PNG
|
||||||
("gImage_UI0902_TabSetting_Sel_36x28", "03_底部图标/底部图标-10.png"),
|
("gImage_UI0902_TabSetting_Sel_40x32", "03_底部图标/底部图标-10.png"), # 万能白
|
||||||
("gImage_UI0902_TabSetting_Not_35x28", "03_底部图标/底部图标-11.png"),
|
("gImage_UI0902_TabSetting_Not_40x32", "03_底部图标/底部图标-11.png"), # 普通白
|
||||||
("gImage_UI0902_TabMixer_Sel_35x27", "03_底部图标/底部图标-12.png"),
|
("gImage_UI0902_TabMixer_Sel_40x32", "03_底部图标/底部图标-12.png"), # 专业白
|
||||||
("gImage_UI0902_TabMixer_Not_18x28", "03_底部图标/底部图标-13.png"),
|
("gImage_UI0902_TabMixer_Not_22x32", "03_底部图标/底部图标-13.png"), # 设置白
|
||||||
("gImage_UI0902_TabMode_Sel_36x28", "03_底部图标/底部图标-14.png"),
|
("gImage_UI0902_TabMode_Sel_40x32", "03_底部图标/底部图标-14.png"), # 万能蓝
|
||||||
("gImage_UI0902_TabMode_Not_35x29", "03_底部图标/底部图标-15.png"),
|
("gImage_UI0902_TabMode_Not_40x32", "03_底部图标/底部图标-15.png"), # 普通蓝
|
||||||
("gImage_UI0902_TabBack_Sel_35x27", "03_底部图标/底部图标-16.png"),
|
("gImage_UI0902_TabBack_Sel_40x32", "03_底部图标/底部图标-16.png"), # 专业蓝
|
||||||
("gImage_UI0902_TabBack_Not_18x28", "03_底部图标/底部图标-17.png"),
|
("gImage_UI0902_TabBack_Not_22x32", "03_底部图标/底部图标-17.png"), # 设置蓝
|
||||||
]
|
]
|
||||||
|
|
||||||
# ---------------- external flash (big images / text strips) ----------------
|
# ---------------- external flash (big images / text strips) ----------------
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,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)
|
||||||
|
|
@ -2,21 +2,23 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Pack external W25Q128 tone/logo image for K1 (0903).
|
"""Pack external W25Q128 tone/logo image for K1 (0903).
|
||||||
|
|
||||||
Layout (absolute W25Q128 offsets):
|
Layout (absolute W25Q128 offsets — BIN2/BIN3 are FIXED so FW addresses stay stable):
|
||||||
0x00000000 logo.bin (legacy pad; boot UI uses UI0902 full-screen logo)
|
0x00000000 logo.bin (legacy pad; boot UI uses UI0902 full-screen logo)
|
||||||
0x0000CB70 Charg.bin (legacy pad; charge UI uses UI0902_CHARGE_SCREEN full-screen)
|
0x0000CB70 Charg.bin (legacy pad; charge UI uses UI0902_CHARGE_SCREEN full-screen)
|
||||||
0x0001B8F0 1.bin 普通/专业 31 rhythms
|
0x0001B8F0 1.bin 普通/专业 31 rhythms
|
||||||
0x0009D07D 2.bin 海阔天空
|
0x0009D07D 2.bin 本地曲目(变长,尾部 0xFF 填到 BIN3;<=41KB)
|
||||||
0x000A8607 3.bin 万能模式
|
0x000A71AC 3.bin 万能模式(固定;勿随 2.bin 长度漂移)
|
||||||
|
|
||||||
Outputs:
|
Outputs:
|
||||||
tools/out/extflash_tone_0903.bin
|
tools/out/extflash_tone_0903.bin
|
||||||
|
tools/out/extflash_tone_0903.res (same bytes, .res alias)
|
||||||
|
tools/out/extflash_ALL_tone0903_ui0902.res (tone + pad + ui0902, if ui pack present)
|
||||||
project/inc/ExtFlash_Tone_Addr.h
|
project/inc/ExtFlash_Tone_Addr.h
|
||||||
Doc/音色文件/0903/FLASH_MAP.txt
|
Doc/音色文件/0903/FLASH_MAP.txt
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -25,11 +27,25 @@ ROOT = Path(__file__).resolve().parents[1] # firmware project root
|
||||||
REPO = ROOT.parent.parent # 一诺国际吉他
|
REPO = ROOT.parent.parent # 一诺国际吉他
|
||||||
OUT_DIR = ROOT / "tools" / "out"
|
OUT_DIR = ROOT / "tools" / "out"
|
||||||
OUT_BIN = OUT_DIR / "extflash_tone_0903.bin"
|
OUT_BIN = OUT_DIR / "extflash_tone_0903.bin"
|
||||||
|
OUT_RES = OUT_DIR / "extflash_tone_0903.res"
|
||||||
|
OUT_ALL_RES = OUT_DIR / "extflash_ALL_tone0903_ui0902.res"
|
||||||
OUT_HDR = ROOT / "project" / "inc" / "ExtFlash_Tone_Addr.h"
|
OUT_HDR = ROOT / "project" / "inc" / "ExtFlash_Tone_Addr.h"
|
||||||
OUT_MAP = REPO / "Doc" / "音色文件" / "0903" / "FLASH_MAP.txt"
|
OUT_MAP = REPO / "Doc" / "音色文件" / "0903" / "FLASH_MAP.txt"
|
||||||
|
UI0902_BIN = OUT_DIR / "ui0902_res.bin"
|
||||||
|
|
||||||
TONE_DIR = REPO / "Doc" / "音色文件" / "0903"
|
TONE_DIR = REPO / "Doc" / "音色文件" / "0903"
|
||||||
|
|
||||||
|
# Fixed map — must match project/inc/ExtFlash_Tone_Addr.h consumed by firmware.
|
||||||
|
OFF_LOGO = 0x00000000
|
||||||
|
OFF_CHARGING = 0x0000CB70
|
||||||
|
OFF_BIN1 = 0x0001B8F0
|
||||||
|
OFF_BIN2 = 0x0009D07D
|
||||||
|
OFF_BIN3 = 0x000A71AC # FIXED: never place 3.bin by concatenating after variable 2.bin
|
||||||
|
UI0902_RES_BASE = 0x00100000
|
||||||
|
DAB_MAGIC = b"\xABDAB"
|
||||||
|
MAX_SONG_BIN_BYTES = 41 * 1024 # 曲目文件(2.bin)硬上限 41KB
|
||||||
|
BIN2_SLOT_BYTES = OFF_BIN3 - OFF_BIN2 # 41263; must keep BIN3 fixed
|
||||||
|
|
||||||
|
|
||||||
def find_ziliao() -> Path:
|
def find_ziliao() -> Path:
|
||||||
for p in REPO.iterdir():
|
for p in REPO.iterdir():
|
||||||
|
|
@ -38,29 +54,66 @@ def find_ziliao() -> Path:
|
||||||
raise FileNotFoundError("资料/logo.bin + Charg.bin not found under repo")
|
raise FileNotFoundError("资料/logo.bin + Charg.bin not found under repo")
|
||||||
|
|
||||||
|
|
||||||
|
def dab_ok(blob: bytes, off: int, expect_cnt: int | None = None) -> None:
|
||||||
|
if off + 16 > len(blob):
|
||||||
|
raise SystemExit(f"DAB check @0x{off:X}: past end of pack ({len(blob)})")
|
||||||
|
magic = blob[off : off + 4]
|
||||||
|
cnt = struct.unpack_from("<I", blob, off + 12)[0]
|
||||||
|
if magic != DAB_MAGIC:
|
||||||
|
raise SystemExit(
|
||||||
|
f"DAB check @0x{off:X}: bad magic {magic.hex()} (want {DAB_MAGIC.hex()})"
|
||||||
|
)
|
||||||
|
if expect_cnt is not None and cnt != expect_cnt:
|
||||||
|
raise SystemExit(f"DAB check @0x{off:X}: cnt={cnt} want {expect_cnt}")
|
||||||
|
|
||||||
|
|
||||||
|
def require_region_equals(blob: bytes, off: int, src: bytes, label: str) -> None:
|
||||||
|
end = off + len(src)
|
||||||
|
if end > len(blob):
|
||||||
|
raise SystemExit(f"{label}: pack too short for region @0x{off:X}+{len(src)}")
|
||||||
|
if blob[off:end] != src:
|
||||||
|
raise SystemExit(f"{label}: bytes @0x{off:X} do not match source file")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
ziliao = find_ziliao()
|
ziliao = find_ziliao()
|
||||||
|
bin1_path = TONE_DIR / "1.bin"
|
||||||
|
bin2_path = TONE_DIR / "2.bin"
|
||||||
|
bin3_path = TONE_DIR / "3.bin"
|
||||||
parts = [
|
parts = [
|
||||||
("LOGO", ziliao / "logo.bin", 0x00000000, "legacy pad; boot uses UI0902_BOOT_LOGO"),
|
("LOGO", ziliao / "logo.bin", OFF_LOGO, "legacy pad; boot uses UI0902_BOOT_LOGO"),
|
||||||
("CHARGING", ziliao / "Charg.bin", 0x0000CB70, "legacy pad; UI uses UI0902_CHARGE_SCREEN"),
|
("CHARGING", ziliao / "Charg.bin", OFF_CHARGING, "legacy pad; UI uses UI0902_CHARGE_SCREEN"),
|
||||||
("BIN1_RHYTHM", TONE_DIR / "1.bin", 0x0001B8F0, "普通/专业 31 条节奏"),
|
("BIN1_RHYTHM", bin1_path, OFF_BIN1, "普通/专业 31 条节奏"),
|
||||||
("BIN2_SONG_HAITIAN", TONE_DIR / "2.bin", None, "本地曲目 海阔天空"),
|
("BIN2_SONG_HAITIAN", bin2_path, OFF_BIN2, "本地曲目"),
|
||||||
("BIN3_UNIVERSAL", TONE_DIR / "3.bin", None, "万能模式"),
|
("BIN3_UNIVERSAL", bin3_path, OFF_BIN3, "万能模式(固定偏移)"),
|
||||||
]
|
]
|
||||||
|
|
||||||
blobs = []
|
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)"
|
||||||
|
)
|
||||||
|
if len(bin2_data) > BIN2_SLOT_BYTES:
|
||||||
|
raise SystemExit(
|
||||||
|
f"2.bin too large for fixed BIN3 slot: {len(bin2_data)} > {BIN2_SLOT_BYTES} "
|
||||||
|
f"(would shift 3.bin past 0x{OFF_BIN3:X})"
|
||||||
|
)
|
||||||
|
|
||||||
|
blobs: list[bytes] = []
|
||||||
cursor = 0
|
cursor = 0
|
||||||
rows = []
|
rows: list[tuple[str, int, int, str]] = []
|
||||||
for name, path, force_off, note in parts:
|
for name, path, force_off, note in parts:
|
||||||
data = path.read_bytes()
|
data = path.read_bytes()
|
||||||
if force_off is not None:
|
if cursor > force_off:
|
||||||
if cursor > force_off:
|
raise SystemExit(
|
||||||
raise SystemExit(f"{name}: cursor 0x{cursor:X} past forced 0x{force_off:X}")
|
f"{name}: cursor 0x{cursor:X} past forced 0x{force_off:X} "
|
||||||
if cursor < force_off:
|
f"(previous blob too large; enlarge next fixed gap or shrink prior file)"
|
||||||
pad = force_off - cursor
|
)
|
||||||
blobs.append(b"\xFF" * pad)
|
if cursor < force_off:
|
||||||
cursor = force_off
|
pad = force_off - cursor
|
||||||
rows.append((f"(pad)", force_off - pad, pad, "gap fill 0xFF"))
|
blobs.append(b"\xFF" * pad)
|
||||||
|
rows.append(("(pad)", cursor, pad, "gap fill 0xFF"))
|
||||||
|
cursor = force_off
|
||||||
off = cursor
|
off = cursor
|
||||||
blobs.append(data)
|
blobs.append(data)
|
||||||
cursor += len(data)
|
cursor += len(data)
|
||||||
|
|
@ -69,9 +122,25 @@ def main() -> None:
|
||||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
packed = b"".join(blobs)
|
packed = b"".join(blobs)
|
||||||
OUT_BIN.write_bytes(packed)
|
OUT_BIN.write_bytes(packed)
|
||||||
|
OUT_RES.write_bytes(packed) # 总音色 .bin 同步为 .res
|
||||||
|
|
||||||
# named lookup
|
|
||||||
by_name = {r[0]: r for r in rows if not r[0].startswith("(")}
|
by_name = {r[0]: r for r in rows if not r[0].startswith("(")}
|
||||||
|
bin1_data = bin1_path.read_bytes()
|
||||||
|
bin3_data = bin3_path.read_bytes()
|
||||||
|
|
||||||
|
# Hard integrity gates
|
||||||
|
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_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")
|
||||||
|
|
||||||
hdr = f"""#ifndef __EXTFLASH_TONE_ADDR_H
|
hdr = f"""#ifndef __EXTFLASH_TONE_ADDR_H
|
||||||
#define __EXTFLASH_TONE_ADDR_H
|
#define __EXTFLASH_TONE_ADDR_H
|
||||||
|
|
@ -92,13 +161,13 @@ def main() -> None:
|
||||||
#define EXTFLASH_CHARGING_W 160
|
#define EXTFLASH_CHARGING_W 160
|
||||||
#define EXTFLASH_CHARGING_H 190
|
#define EXTFLASH_CHARGING_H 190
|
||||||
|
|
||||||
#define EXTFLASH_BIN1_RHYTHM_ADDR 0x{by_name['BIN1_RHYTHM'][1]:08X}UL
|
#define EXTFLASH_BIN1_RHYTHM_ADDR 0x{OFF_BIN1:08X}UL
|
||||||
#define EXTFLASH_BIN1_RHYTHM_SIZE {by_name['BIN1_RHYTHM'][2]}UL
|
#define EXTFLASH_BIN1_RHYTHM_SIZE {by_name['BIN1_RHYTHM'][2]}UL
|
||||||
|
|
||||||
#define EXTFLASH_BIN2_SONG_HAITIAN_ADDR 0x{by_name['BIN2_SONG_HAITIAN'][1]:08X}UL
|
#define EXTFLASH_BIN2_SONG_HAITIAN_ADDR 0x{OFF_BIN2:08X}UL
|
||||||
#define EXTFLASH_BIN2_SONG_HAITIAN_SIZE {by_name['BIN2_SONG_HAITIAN'][2]}UL
|
#define EXTFLASH_BIN2_SONG_HAITIAN_SIZE {by_name['BIN2_SONG_HAITIAN'][2]}UL
|
||||||
|
|
||||||
#define EXTFLASH_BIN3_UNIVERSAL_ADDR 0x{by_name['BIN3_UNIVERSAL'][1]:08X}UL
|
#define EXTFLASH_BIN3_UNIVERSAL_ADDR 0x{OFF_BIN3:08X}UL
|
||||||
#define EXTFLASH_BIN3_UNIVERSAL_SIZE {by_name['BIN3_UNIVERSAL'][2]}UL
|
#define EXTFLASH_BIN3_UNIVERSAL_SIZE {by_name['BIN3_UNIVERSAL'][2]}UL
|
||||||
|
|
||||||
/* Convenience aliases used by UI ADDRESS */
|
/* Convenience aliases used by UI ADDRESS */
|
||||||
|
|
@ -121,8 +190,11 @@ def main() -> None:
|
||||||
map_lines = [
|
map_lines = [
|
||||||
"K1 external Flash map — tone pack 0903",
|
"K1 external Flash map — tone pack 0903",
|
||||||
f"Packed file: Code/.../tools/out/{OUT_BIN.name} ({len(packed)} bytes)",
|
f"Packed file: Code/.../tools/out/{OUT_BIN.name} ({len(packed)} bytes)",
|
||||||
|
f"Also: {OUT_RES.name}; ALL.res when ui0902_res.bin present",
|
||||||
f"Ends at 0x{cursor:X}; UI0902_RES_BASE=0x00100000; free gap={0x100000 - cursor} bytes",
|
f"Ends at 0x{cursor:X}; UI0902_RES_BASE=0x00100000; free gap={0x100000 - cursor} bytes",
|
||||||
"",
|
"",
|
||||||
|
"NOTE: BIN3 is FIXED at 0x000A71AC. 2.bin must be <= 41KB and <= slot 41263 bytes.",
|
||||||
|
"",
|
||||||
f"{'Name':<22} {'Offset':>10} {'Size':>10} Note",
|
f"{'Name':<22} {'Offset':>10} {'Size':>10} Note",
|
||||||
"-" * 72,
|
"-" * 72,
|
||||||
]
|
]
|
||||||
|
|
@ -131,31 +203,52 @@ def main() -> None:
|
||||||
map_lines += [
|
map_lines += [
|
||||||
"",
|
"",
|
||||||
"Firmware ADDRESS mapping:",
|
"Firmware ADDRESS mapping:",
|
||||||
" 普通/专业 -> FLASH_ADDR_MODE_NORMAL/EXPERT (1.bin)",
|
" 普通/专业 -> FLASH_ADDR_MODE_NORMAL/EXPERT (1.bin @ 0x1B8F0)",
|
||||||
" 海阔天空 -> FLASH_ADDR_SONG_HAITIAN (2.bin)",
|
" 本地曲目 -> FLASH_ADDR_SONG_HAITIAN (2.bin @ 0x9D07D, max 41KB)",
|
||||||
" 万能 -> FLASH_ADDR_MODE_UNIVERSAL (3.bin)",
|
" 万能 -> FLASH_ADDR_MODE_UNIVERSAL (3.bin @ 0xA71AC FIXED)",
|
||||||
" AutoBand -> FLASH_ADDR_AUTOBAND_LEGACY 0x9EB5F (unchanged; overlaps 2.bin region — do not enable until remapped)",
|
" AutoBand -> FLASH_ADDR_AUTOBAND_LEGACY 0x9EB5F (unchanged; overlaps 2.bin region — do not enable until remapped)",
|
||||||
" Boot logo -> UI0902_BOOT_LOGO_ADDR (full-screen); packed logo.bin only pads 0x0..0xCB70",
|
" Boot logo -> UI0902_BOOT_LOGO_ADDR (full-screen); packed logo.bin only pads 0x0..0xCB70",
|
||||||
" Charging -> EXTFLASH_CHARGING_ADDR",
|
" Charging -> EXTFLASH_CHARGING_ADDR",
|
||||||
"",
|
"",
|
||||||
"Add a local song:",
|
"Add a local song:",
|
||||||
" 1. Pack the new preset into 2.bin",
|
" 1. Pack the new preset into 2.bin (must stay <= 41KB / 41263 slot)",
|
||||||
" 2. Append a row to local_songs.csv (index,code,name)",
|
" 2. Append a row to local_songs.csv (index,code,name)",
|
||||||
" 3. python tools/pack_extflash_tone_0903.py (also regenerates LocalSongNames.h)",
|
" 3. python tools/pack_extflash_tone_0903.py (also regenerates LocalSongNames.h)",
|
||||||
" 4. Rebuild firmware and flash MCU + ExtFlash",
|
" 4. Rebuild firmware and flash MCU + ExtFlash ALL.res",
|
||||||
]
|
]
|
||||||
OUT_MAP.write_text("\n".join(map_lines) + "\n", encoding="utf-8", newline="\n")
|
OUT_MAP.write_text("\n".join(map_lines) + "\n", encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
print(f"Wrote {OUT_BIN} ({len(packed)} bytes)")
|
print(f"Wrote {OUT_BIN} ({len(packed)} bytes)")
|
||||||
|
print(f"Wrote {OUT_RES} ({len(packed)} bytes)")
|
||||||
print(f"Wrote {OUT_HDR}")
|
print(f"Wrote {OUT_HDR}")
|
||||||
print(f"Wrote {OUT_MAP}")
|
print(f"Wrote {OUT_MAP}")
|
||||||
for name, off, size, note in rows:
|
for name, off, size, note in rows:
|
||||||
print(f" 0x{off:08X} {size:8d} {name} {note}")
|
print(f" 0x{off:08X} {size:8d} {name} {note}")
|
||||||
if cursor > 0x00100000:
|
if cursor > UI0902_RES_BASE:
|
||||||
raise SystemExit("ERROR: pack overflows into UI0902_RES_BASE")
|
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: BIN1/BIN2/BIN3 present at fixed addresses and match source files")
|
||||||
|
print(f"OK: 2.bin size {len(bin2_data)} <= 41KB ({MAX_SONG_BIN_BYTES})")
|
||||||
|
|
||||||
|
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)
|
||||||
|
OUT_ALL_RES.write_bytes(all_res)
|
||||||
|
# Mirror under repo tools/out for publish/consumers
|
||||||
|
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_RES.name).write_bytes(packed)
|
||||||
|
print(f"Wrote {OUT_ALL_RES} ({len(all_res)} bytes) — verified 1/2/3.bin")
|
||||||
|
else:
|
||||||
|
print(f"WARN: {UI0902_BIN} missing; skipped ALL.res")
|
||||||
|
|
||||||
# Keep LocalSongNames.h in sync with Doc/.../local_songs.csv when packing tones.
|
|
||||||
gen = ROOT / "tools" / "gen_local_song_names.py"
|
gen = ROOT / "tools" / "gen_local_song_names.py"
|
||||||
r = subprocess.run([sys.executable, str(gen)], check=False)
|
r = subprocess.run([sys.executable, str(gen)], check=False)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Build K1 release zip: MCU .bin + ExtFlash resources + SoundWalkerIAP + docs.
|
"""Build K1 release zip: Boot + MCU APP + ExtFlash resources + SoundWalkerIAP + docs.
|
||||||
|
|
||||||
External Flash has TWO regions that must both be present on a blank/erased chip:
|
External Flash has TWO regions that must both be present on a blank/erased chip:
|
||||||
0x00000000 tone pack (logo pad + Charg + 1/2/3.bin) -> *.tone.res / combined
|
0x00000000 tone pack (logo pad + Charg + 1/2/3.bin) -> *.tone.res / combined
|
||||||
0x00100000 UI0902 bitmaps (mode rows, boot logo, ...) -> *.ui0902.res / combined
|
0x00100000 UI0902 bitmaps (mode rows, boot logo, ...) -> *.ui0902.res / combined
|
||||||
|
|
||||||
Prefer flashing the combined *.extflash.res @ 0x0 for colleague upgrades.
|
Prefer flashing the combined *.extflash.res @ 0x0 for colleague upgrades.
|
||||||
|
Boot must also be updated so USB IAP wait screen can show UI0902_FLASH_MODE.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -19,15 +20,26 @@ from pathlib import Path
|
||||||
REPO = Path(r"C:\Users\qjyu\Documents\SoundWalker\一诺国际吉他")
|
REPO = Path(r"C:\Users\qjyu\Documents\SoundWalker\一诺国际吉他")
|
||||||
PROJ = REPO / "Code" / "YNGJ-GT1-M - AT32F403ARCT7"
|
PROJ = REPO / "Code" / "YNGJ-GT1-M - AT32F403ARCT7"
|
||||||
EXE_BIN = PROJ / "project" / "IAR_V7.4" / "YNGJ-GT1-M" / "Exe" / "YNGJ-GT1-M.bin"
|
EXE_BIN = PROJ / "project" / "IAR_V7.4" / "YNGJ-GT1-M" / "Exe" / "YNGJ-GT1-M.bin"
|
||||||
|
BOOT_BIN = (
|
||||||
|
PROJ
|
||||||
|
/ "AT32F403ARCT7_BOOT"
|
||||||
|
/ "project"
|
||||||
|
/ "IAR_V7.4"
|
||||||
|
/ "AT32F403ARCT7_BOOT"
|
||||||
|
/ "Exe"
|
||||||
|
/ "AT32F403ARCT7_BOOT.bin"
|
||||||
|
)
|
||||||
TONE_BIN = PROJ / "tools" / "out" / "extflash_tone_0903.bin"
|
TONE_BIN = PROJ / "tools" / "out" / "extflash_tone_0903.bin"
|
||||||
UI0902_BIN = PROJ / "tools" / "out" / "ui0902_res.bin"
|
UI0902_BIN = PROJ / "tools" / "out" / "ui0902_res.bin"
|
||||||
MAP_TXT = REPO / "Doc" / "音色文件" / "0903" / "FLASH_MAP.txt"
|
MAP_TXT = REPO / "Doc" / "音色文件" / "0903" / "FLASH_MAP.txt"
|
||||||
IAP_DIR = REPO / "升级" / "MCU主控升级"
|
IAP_DIR = REPO / "升级" / "MCU主控升级"
|
||||||
OUT_ROOT = REPO / "tools" / "out"
|
OUT_ROOT = REPO / "tools" / "out"
|
||||||
|
|
||||||
FW_VER = "0.2.6"
|
FW_VER = "0.2.8"
|
||||||
TONE_RES_VER = "0903"
|
TONE_RES_VER = "0903"
|
||||||
UI0902_RES_BASE = 0x00100000
|
UI0902_RES_BASE = 0x00100000
|
||||||
|
BOOT_FLASH_ADDR = 0x08000000
|
||||||
|
APP_FLASH_ADDR = 0x08008000
|
||||||
|
|
||||||
|
|
||||||
def git_info(cwd: Path) -> tuple[str, str]:
|
def git_info(cwd: Path) -> tuple[str, str]:
|
||||||
|
|
@ -57,7 +69,36 @@ def build_combined_extflash(tone: bytes, ui: bytes) -> bytes:
|
||||||
return tone + (b"\xFF" * pad) + ui
|
return tone + (b"\xFF" * pad) + ui
|
||||||
|
|
||||||
|
|
||||||
|
def verify_tone_layout(tone: bytes, check_sources: bool = True) -> None:
|
||||||
|
"""Refuse to ship ALL.res if 1/2/3.bin missing, shifted, or content mismatch."""
|
||||||
|
dab = b"\xABDAB"
|
||||||
|
sources = {
|
||||||
|
"BIN1": (0x0001B8F0, 31, REPO / "Doc" / "音色文件" / "0903" / "1.bin"),
|
||||||
|
"BIN2": (0x0009D07D, 1, REPO / "Doc" / "音色文件" / "0903" / "2.bin"),
|
||||||
|
"BIN3": (0x000A71AC, 3, REPO / "Doc" / "音色文件" / "0903" / "3.bin"),
|
||||||
|
}
|
||||||
|
for name, (off, want_cnt, src_path) in sources.items():
|
||||||
|
if off + 16 > len(tone):
|
||||||
|
raise SystemExit(f"verify {name}: tone pack too short for 0x{off:X}")
|
||||||
|
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: BIN1/BIN2/BIN3 addresses + content OK")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
if not BOOT_BIN.is_file():
|
||||||
|
raise SystemExit(f"missing Boot bin: {BOOT_BIN}")
|
||||||
if not EXE_BIN.is_file():
|
if not EXE_BIN.is_file():
|
||||||
raise SystemExit(f"missing MCU bin: {EXE_BIN}")
|
raise SystemExit(f"missing MCU bin: {EXE_BIN}")
|
||||||
if not TONE_BIN.is_file():
|
if not TONE_BIN.is_file():
|
||||||
|
|
@ -83,15 +124,20 @@ def main() -> None:
|
||||||
pkg.mkdir(parents=True)
|
pkg.mkdir(parents=True)
|
||||||
|
|
||||||
tone_data = TONE_BIN.read_bytes()
|
tone_data = TONE_BIN.read_bytes()
|
||||||
|
verify_tone_layout(tone_data)
|
||||||
ui_data = UI0902_BIN.read_bytes()
|
ui_data = UI0902_BIN.read_bytes()
|
||||||
combined = build_combined_extflash(tone_data, ui_data)
|
combined = build_combined_extflash(tone_data, ui_data)
|
||||||
|
verify_tone_layout(combined) # same offsets in ALL.res
|
||||||
|
|
||||||
|
boot_name = f"AT32F403ARCT7_BOOT_v{FW_VER}_{stamp}.bin"
|
||||||
mcu_name = f"YNGJ-GT1-M_MCU_v{FW_VER}_{stamp}.bin"
|
mcu_name = f"YNGJ-GT1-M_MCU_v{FW_VER}_{stamp}.bin"
|
||||||
comb_name = f"extflash_ALL_tone0903_ui0902_{stamp}.res"
|
comb_name = f"extflash_ALL_tone0903_ui0902_{stamp}.res"
|
||||||
|
|
||||||
|
boot_dst = pkg / boot_name
|
||||||
mcu_dst = pkg / mcu_name
|
mcu_dst = pkg / mcu_name
|
||||||
comb_dst = pkg / comb_name
|
comb_dst = pkg / comb_name
|
||||||
|
|
||||||
|
shutil.copy2(BOOT_BIN, boot_dst)
|
||||||
shutil.copy2(EXE_BIN, mcu_dst)
|
shutil.copy2(EXE_BIN, mcu_dst)
|
||||||
comb_dst.write_bytes(combined)
|
comb_dst.write_bytes(combined)
|
||||||
shutil.copy2(iap, pkg / "SoundWalkerIAP.exe")
|
shutil.copy2(iap, pkg / "SoundWalkerIAP.exe")
|
||||||
|
|
@ -99,6 +145,7 @@ def main() -> None:
|
||||||
if MAP_TXT.is_file():
|
if MAP_TXT.is_file():
|
||||||
shutil.copy2(MAP_TXT, pkg / "FLASH_MAP.txt")
|
shutil.copy2(MAP_TXT, pkg / "FLASH_MAP.txt")
|
||||||
|
|
||||||
|
shutil.copy2(boot_dst, OUT_ROOT / boot_name)
|
||||||
shutil.copy2(mcu_dst, OUT_ROOT / mcu_name)
|
shutil.copy2(mcu_dst, OUT_ROOT / mcu_name)
|
||||||
shutil.copy2(comb_dst, OUT_ROOT / "extflash_ALL_tone0903_ui0902.res")
|
shutil.copy2(comb_dst, OUT_ROOT / "extflash_ALL_tone0903_ui0902.res")
|
||||||
# Keep build intermediates under tools/out for local rebuilds; not shipped in package.
|
# Keep build intermediates under tools/out for local rebuilds; not shipped in package.
|
||||||
|
|
@ -115,26 +162,34 @@ def main() -> None:
|
||||||
f"Git subject {subject}",
|
f"Git subject {subject}",
|
||||||
f"FW version {FW_VER}",
|
f"FW version {FW_VER}",
|
||||||
"",
|
"",
|
||||||
"==== 整机升级请刷这两项 ====",
|
"==== 整机升级请刷这三项 ====",
|
||||||
f" 1) {mcu_name} -> MCU APP",
|
f" 1) {boot_name} -> Bootloader @ 0x{BOOT_FLASH_ADDR:08X}",
|
||||||
f" 2) {comb_name} -> 外部 Flash 从 0x0 起整包",
|
f" 2) {mcu_name} -> MCU APP @ 0x{APP_FLASH_ADDR:08X}",
|
||||||
|
f" 3) {comb_name} -> 外部 Flash 从 0x0 起整包",
|
||||||
" (= 音色区 + 填充 + UI0902 图片区)",
|
" (= 音色区 + 填充 + UI0902 图片区)",
|
||||||
"",
|
"",
|
||||||
"建议步骤:",
|
"建议步骤:",
|
||||||
|
" - 先刷 Boot(否则 USB 烧录等待画面仍是旧红字「升级模式」)",
|
||||||
|
" - 再刷 APP",
|
||||||
" - 先整片擦除外部 Flash,再刷上述 ALL .res @ 0x00000000",
|
" - 先整片擦除外部 Flash,再刷上述 ALL .res @ 0x00000000",
|
||||||
" - 勿只刷音色区,否则模式选择页会花屏(UI 图在 0x100000)",
|
" - 勿只刷音色区,否则模式选择页会花屏(UI 图在 0x100000)",
|
||||||
"",
|
"",
|
||||||
"外部 Flash 分区:",
|
"外部 Flash 分区:",
|
||||||
" 0x00000000 音色/充电图 (toneRes)",
|
" 0x00000000 音色/充电图 (toneRes)",
|
||||||
" logo pad + Charg@0xCB70 + 1.bin@0x1B8F0 + 2.bin(HKTK/2s) + 3.bin",
|
" logo pad + Charg@0xCB70 + 1.bin@0x1B8F0 + 2.bin@0x9D07D + 3.bin@0xA71AC(FIXED)",
|
||||||
" 0x00100000 UI0902 图片 (模式选择、开机 Logo、充电、烧录模式、设置/调音台等)",
|
" 0x00100000 UI0902 图片 (模式选择、开机 Logo、充电、烧录模式、设置/调音台等)",
|
||||||
|
" 0x001D2000 UI0902_FLASH_MODE 烧录模式全屏图(Boot 读取)",
|
||||||
"",
|
"",
|
||||||
|
f" {boot_name} size={boot_dst.stat().st_size} @ 0x{BOOT_FLASH_ADDR:08X}",
|
||||||
|
f" sha256={sha256(boot_dst)}",
|
||||||
|
f" {mcu_name} size={mcu_dst.stat().st_size} @ 0x{APP_FLASH_ADDR:08X}",
|
||||||
|
f" sha256={sha256(mcu_dst)}",
|
||||||
f" {comb_name} size={comb_dst.stat().st_size} @ 0x00000000",
|
f" {comb_name} size={comb_dst.stat().st_size} @ 0x00000000",
|
||||||
f" sha256={sha256(comb_dst)}",
|
f" sha256={sha256(comb_dst)}",
|
||||||
"",
|
"",
|
||||||
f"MCU: {mcu_name} size={mcu_dst.stat().st_size} sha256={sha256(mcu_dst)}",
|
|
||||||
"",
|
|
||||||
"说明:",
|
"说明:",
|
||||||
|
" - Boot 进入 USB IAP 时显示 UI0902_FLASH_MODE;资源未烧录时白字黑底兜底。",
|
||||||
|
" - 烧录模式图来源:K1标准界面图 0904/烧录模式.png。",
|
||||||
" - 开机全屏 Logo、关机充电全屏画面在 UI0902,不在音色包里的 logo.bin/Charg.bin 占位。",
|
" - 开机全屏 Logo、关机充电全屏画面在 UI0902,不在音色包里的 logo.bin/Charg.bin 占位。",
|
||||||
" - AutoBand 0x9EB5F 本版未重排。",
|
" - AutoBand 0x9EB5F 本版未重排。",
|
||||||
"",
|
"",
|
||||||
|
|
|
||||||
|
|
@ -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,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,589 @@
|
||||||
|
#!/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
|
||||||
|
):
|
||||||
|
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,495 @@
|
||||||
|
# -*- 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}")
|
||||||
|
sim = BleMidiSim(
|
||||||
|
name=args.ble_name,
|
||||||
|
address=args.ble_address,
|
||||||
|
transport=args.transport,
|
||||||
|
unpair=args.unpair,
|
||||||
|
)
|
||||||
|
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
|
||||||