Compare commits

..

32 Commits

Author SHA1 Message Date
yuquanjun 1cf6562236 Add bass/chord register mapping with RTT inject and bin1/3 probes.
Intercept AutoBand MIDI by channel (bass ch8, drum ch9), add chord-key RTT cmds, and ship scripts to verify 1.bin/3.bin pitch rules on device.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 21:21:11 +08:00
yuquanjun 91b270d856 Revert "Lower accompaniment by one octave for #IV and V-VII chords."
This reverts commit 439ccf439d.
2026-09-11 14:59:23 +08:00
yuquanjun 439ccf439d Lower accompaniment by one octave for #IV and V-VII chords.
AutoBand ignores input octave, so shift engine MIDI output by -12 for those chords (except drums) to keep the range from low #IV up to natural IV.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 22:08:09 +08:00
yuquanjun 2d62e1e41a Stop drawing status volume bar on mixer page to avoid erasing the back button.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 19:29:47 +08:00
yuquanjun 4aad989263 Reapply master volume at boot/mode-entry/play-start and sync volume bar on settings hub.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 18:58:24 +08:00
yuquanjun 295a2a6fa3 Fix song-name trailing ghost by clearing the full text row before redraw.
When switching from a longer title to a shorter one, the old gradient-only clear left uncovered glyph fragments outside the pill bounds.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 10:46:11 +08:00
yuquanjun b65b762d92 Refresh git_user_fw_ver.h after 0.2.8 release rebuild.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 07:03:20 +08:00
yuquanjun 30c4ea9c0d Refresh git_user_fw_ver.h for 0.2.8 release commit.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 07:02:22 +08:00
yuquanjun 05ccf28a4b Release 0.2.8: harden BLE SysEx receive path for app stability.
Bump firmware version after BT recv stack/log fixes and pitch/chord bounds so Phoenix Atelier map/LED traffic no longer stack-smashes the MCU.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 07:02:22 +08:00
yuquanjun 59c11f3f93 Fix BT recv stack smash and harden pitch/chord SysEx paths.
Enlarge BT recv stack, move large handlers/log buffers off the stack, clamp key indexes, and yield in W25Q busy-wait so BLE protocol tests stay stable.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 06:51:32 +08:00
yuquanjun 24ffe87527 Refresh bottom nav with 0909 enlarged icons and aligned label art.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 20:24:39 +08:00
yuquanjun 5bb403cd11 Add artist self-serve tone pack tool for USB IAP updates of 1/2/3.bin.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 16:37:29 +08:00
yuquanjun a818065c2e Fix mixer knob outline at max volume by seating the knob on the track capsule ends.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 16:33:03 +08:00
yuquanjun c1b32c0351 Refresh git_user_fw_ver.h for 0.2.7 release commit.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 12:18:00 +08:00
yuquanjun bb6d9ae7f9 Release 0.2.7: refresh rhythm 1.bin pack with fixed BIN3 layout checks.
Update pack/publish to enforce 41KB song limit, verify 1/2/3.bin at fixed addresses, and emit .res aliases for the tone and ALL extflash images.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 12:17:42 +08:00
yuquanjun 99f7ed3e5a Pin BIN3 to fixed 0xA71AC and reject packs where 2.bin drift shifts it.
Variable 2.bin length previously moved 3.bin; firmware still read 0xA71AC and saw zeros. Pack now pads to the fixed slot and publish verifies DAB headers before shipping ALL.res.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 11:47:15 +08:00
yuquanjun cd21e4e45e Always enable loop on universal-mode pick so accompaniment can play.
Without SetLoopMode the piece path often produced no audible output when the chord pad was not held.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 11:35:14 +08:00
yuquanjun 92e5e32de3 Diagnose BIN3 LoadPreset failures and add flash bin3 tooling.
Log Flash magic on load failure, expose RTT flash bin3, and document the 0903 pack offset for the universal bank.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 11:32:40 +08:00
yuquanjun 015417dc3d Enlarge bottom nav touch zones to full-width 60x55 cells under the section buttons.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 11:01:40 +08:00
yuquanjun 6f2f7b6b75 Remove mode-select top stripe cover that left an extra line.
The ROW_BG full-width fill over the universal row top corners showed as a second separator under the status bar; source art no longer needs it.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 08:00:06 +08:00
yuquanjun ba110565f1 Use chevron asset for factory-reset row instead of solid triangle.
Matches 0902-07: restore is a drill-in row with thin > (ICON_CHEVRON), not the prev/next filled triangle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 07:50:08 +08:00
yuquanjun ad0d990c55 Center indexed param/timbre labels as one block and nudge section digits.
Fixes the left-biased index+name layout, unifies index-name gap across rows, and shifts play-section numbers +2px per UI markup.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 07:34:48 +08:00
yuquanjun f1f9520637 Fix settings BT/restore icons and soft-off BLE protocol plumbing.
Point system-settings rows at the correct Flash icons, keep Main/UI alive across soft-off, and expose git build id for 01 0C with matching BLE tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 07:03:52 +08:00
yuquanjun 57448b3da8 Harden BLE protocol test for Windows pairing and flaky notifies.
Default --unpair on Windows, probe/reconnect on first timeout, resync FrameParser on duplicate F0, and retry query once.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 22:14:18 +08:00
yuquanjun 0aedb45e6e Make BLE SysEx UID and fw-code MIDI-safe for BLE-MIDI.
Encode 01 07 UID as hex ASCII and use 01 0F (keep 01 FF alias) so high-bit bytes are not dropped as timestamps.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 22:02:06 +08:00
yuquanjun 7b08f5e098 Start BLE UART4 App protocol receive on power-on.
TaskBTRecvThread previously started only after mode select, so GATT SysEx never reached the parser until then; verified BLE-MIDI 01 01 now replies with GATT notify.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 21:46:14 +08:00
yuquanjun f762348883 Merge branch 'main' into develop 2026-09-08 18:15:45 +08:00
yuquanjun 8febbd7742 Align BLE SysEx style/transpose replies with Doc protocol and add BLE test harness.
Drop ineffective Dream BT rename SysEx; fix 03 04 boot/list/total and 03 07 read length.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 18:05:04 +08:00
yuquanjun bd22246942 Use AT32 UID for unique PHON_ name and push it to Dream over SysEx.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 12:37:29 +08:00
yuquanjun 725ac495ba Merge main: bring Release 0.2.5 (2ss.bin tone pack) into develop.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 10:18:06 +08:00
yuquanjun 01bc2a592c Implement full BLE SysEx protocol handlers and app-sim test.
Wire missing 01/02/03/05 commands, fix chord-map reply and section/End dispatch, and add UART4 protocol regression script.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 10:00:55 +08:00
yuquanjun a6259b5fca Release 0.2.4: package Boot with UI0902 flash-mode screen.
EOF

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 05:51:58 +08:00
52 changed files with 6056 additions and 1177 deletions

View File

@ -303,6 +303,14 @@ void app_adc_sync_volume(uint16_t val)
app_adc_volume_map_level(val);
}
/* 取当前主音量(复用扫描任务维护的最近一次有效值)。
* ADC
* vol_last0xFFFF */
uint8_t app_adc_get_volume(void)
{
return (vol_last > 127) ? 0 : (uint8_t)vol_last;
}
static void app_adc_volume_process(void)
{
uint16_t val = ADC_ReadChannel(ADC_CHANNEL_1)>>5;

View File

@ -27,4 +27,6 @@ void app_adc_in1_scan_part(uint8_t channel);
bool app_adc_usb_is_plugged(void);
/* 开机/主动采样后同步 vol_last 与 level避免扫描任务误触发或跳变 */
void app_adc_sync_volume(uint16_t val);
/* 取当前主音量0~127供 MasterVolume_Reapply 等重发场景使用 */
uint8_t app_adc_get_volume(void);
#endif

View File

@ -11,7 +11,7 @@ static uint32_t s_overflow;
static uint32_t s_boot_count;
static char s_ui_page[16] = "boot";
static char s_debug_level = 'D';
static char s_cmd_buf[48];
static char s_cmd_buf[64];
static uint8_t s_cmd_len;
/* RTT 二进制烧录:写到 W25Q128ui0902 或本地曲目) */
@ -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, ...)
{
char line[APP_LOG_SLOT_SIZE];
/* 静态行缓冲TaskBTRecv 等小栈线程禁止再开 128B 局部数组 */
static char line[APP_LOG_SLOT_SIZE];
static rt_mutex_t s_log_mtx = RT_NULL;
va_list ap;
int n;
@ -144,9 +146,21 @@ void app_log_write(char level, const char *cat, const char *fmt, ...)
return;
}
if (s_log_mtx == RT_NULL) {
s_log_mtx = rt_mutex_create("applog", RT_IPC_FLAG_PRIO);
}
if (s_log_mtx != RT_NULL) {
if (rt_mutex_take(s_log_mtx, rt_tick_from_millisecond(20)) != RT_EOK) {
return;
}
}
n = snprintf(line, sizeof(line), "[%05u][%c][%-4s] ",
(unsigned)app_log_ms(), level, cat);
if (n < 0) {
if (s_log_mtx != RT_NULL) {
rt_mutex_release(s_log_mtx);
}
return;
}
if ((size_t)n >= sizeof(line)) {
@ -160,6 +174,10 @@ void app_log_write(char level, const char *cat, const char *fmt, ...)
app_log_slot_store(line);
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "\n");
if (s_log_mtx != RT_NULL) {
rt_mutex_release(s_log_mtx);
}
}
static void app_log_dump_ram(void)
@ -280,6 +298,12 @@ uint8_t app_log_try_command(const char *cmd)
"FLASH_HAITIAN_GO", "FLASH_HAITIAN_OK\n");
return 1U;
}
if (strncmp(cmd, "flash bin3 ", 11) == 0) {
uint32_t size = (uint32_t)strtoul(cmd + 11, NULL, 10);
app_log_flash_start_at(EXTFLASH_BIN3_UNIVERSAL_ADDR, size,
"FLASH_BIN3_GO", "FLASH_BIN3_OK\n");
return 1U;
}
if (strncmp(cmd, "tone status", 11) == 0) {
uint8_t hdr[16];
uint8_t name[40];
@ -320,6 +344,16 @@ uint8_t app_log_try_command(const char *cmd)
(unsigned long)(cur_hdr[12] | (cur_hdr[13] << 8) |
(cur_hdr[14] << 16) | (cur_hdr[15] << 24)));
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
{
uint8_t b3[16];
W25Q128_Read(b3, EXTFLASH_BIN3_UNIVERSAL_ADDR, 16);
snprintf(line, sizeof(line),
"TONE @BIN3 magic=%02X%02X%02X%02X cnt=%lu\n",
b3[0], b3[1], b3[2], b3[3],
(unsigned long)(b3[12] | (b3[13] << 8) |
(b3[14] << 16) | (b3[15] << 24)));
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
}
snprintf(line, sizeof(line),
"TONE @BIN2 magic=%02X%02X%02X%02X max=%lu cnt=%lu name=%.16s\n",
hdr[0], hdr[1], hdr[2], hdr[3],
@ -364,16 +398,126 @@ uint8_t app_log_try_command(const char *cmd)
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
return 1U;
}
/* tone bin1 [idx] → 1.bin 节奏(专业+节奏类型) */
if (strncmp(cmd, "tone bin1", 9) == 0) {
char line[160];
int ret;
unsigned idx = 0;
int count;
if (cmd[9] == ' ' && cmd[10] != '\0')
idx = (unsigned)strtoul(cmd + 10, NULL, 10);
mGuiData[GUI_TAB_INDEX].Current = 1; /* 专业 */
mGuiData[GUI_AUTOBAND_SW].Current = 0; /* 节奏类型→1.bin */
ParamGuiData[SONG_MODE_PARAM].Current = (uint8_t)idx;
UI_ApplyToneAddress();
AutoBandTop1_Stop();
StartFlag = 0;
count = AutoBandTop1_GetPresetItemCount();
if (count > 0 && (int)idx >= count)
idx = (unsigned)(count - 1);
ParamGuiData[SONG_MODE_PARAM].Current = (uint8_t)idx;
ret = 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) {
/* 模拟专业+本地曲目拨片(无和弦板) */
/* 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_AUTOBAND_SW].Current = 1;
KEY_ID_1629 = 0; /* 走默认和弦兜底 */
if (!(cmd[9] == ' ' && (cmd[10] == 'k' || cmd[10] == 'K'))) {
KEY_ID_1629 = 0; /* 默认走一级和弦兜底 */
}
StartFlag = 0;
Pick_Handle();
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_PICK_DONE\n");
return 1U;
}
/* 测试注入chord key N0释放 / 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 N0~11C=0 */
if (strncmp(cmd, "chord xpose ", 12) == 0 && cmd[12] != '\0') {
unsigned xp = (unsigned)strtoul(cmd + 12, NULL, 10);
char line[48];
if (xp > 11U) {
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "CHORD_XPOSE_BAD\n");
return 1U;
}
mGuiData[GUI_TRANSPOSE].Current = (uint8_t)xp;
snprintf(line, sizeof(line), "CHORD_XPOSE_OK xp=%u\n", xp);
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
LOG_I("REG", "xpose set %u", xp);
return 1U;
}
if (strncmp(cmd, "flash erase chip", 16) == 0) {
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "FLASH_ERASE_BEGIN\n");
LOG_I("FLASH", "W25Q128 chip erase start");

View File

@ -38,6 +38,13 @@ uint8_t app_tm1629_Scan_Key(void)
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)
{

View File

@ -4,6 +4,7 @@
void app_tm1629_init(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_led(LedNum_TypeDef led, LedColor_TypeDef color);
void app_tm1629_all_off(void);

View File

@ -89,11 +89,11 @@ const Touch_AreaTypeDef Touch_Areas[] =
{ 126, 172, UI0902_SECTION_Y, UI0902_SECTION_Y + UI0902_SECTION_BTN_H - 1, 0, 2, GUI_PLAY_SECTION, NULL },
{ 183, 229, UI0902_SECTION_Y, UI0902_SECTION_Y + UI0902_SECTION_BTN_H - 1, 0, 3, GUI_PLAY_SECTION, NULL },
/* 底栏四键:万能 | 普通 | 专业 | 设置(触区中线对齐 cx 40/100/160/210 */
{ 0, 69, 280, 319, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
{ 70, 129, 280, 319, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
{ 130, 184, 280, 319, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
{ 185, 239, 280, 319, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
/* 底栏四键:万能 | 普通 | 专业 | 设置(60×55y265319 */
{ UI0902_NAV_X0(0), UI0902_NAV_X1(0), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
{ UI0902_NAV_X0(1), UI0902_NAV_X1(1), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
{ UI0902_NAV_X0(2), UI0902_NAV_X1(2), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
{ UI0902_NAV_X0(3), UI0902_NAV_X1(3), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
};
/* 推子 0~10 共 11 档档10 取 AW816 上限附近 */
@ -101,7 +101,7 @@ int16_t MIC_VOL_MAP[11]= {-9000,-7000,-5000,-3000,-1000,1000,3000,5000,7000,9000
uint8_t TOUCH_AREA_NUM = (sizeof(Touch_Areas)/sizeof(Touch_AreaTypeDef));
const uint8_t Version[3] = {0,2,6}; /* major.minor.patch — 0.2.6: local song 2.bin from Doc/音色文件/0908/2ss_2.bin */
const uint8_t Version[3] = {0,2,8}; /* major.minor.patch — 0.2.8: BT recv stack/log harden; pitch/chord SysEx bounds */
uint8_t Led = 8;
bool PressFlag = 0;
@ -184,6 +184,7 @@ void System_PowerOn(void)
BSP_MainPowerEnable(1);
BSP_DreamCorePowerEnable(1);
BSP_HT7178PowerEnable(1);
/* 蓝牙按 NVM/系统设置开关恢复,勿强制常开 */
BSP_BlueToothPowerEnable((uint8_t)(mGuiData[GUI_BL_SW].Current ? 1 : 0));
powon = true; /* 更新开机标志 */
@ -196,7 +197,9 @@ void System_PowerOn(void)
app_tm1617_init();
XPT2046_Init();
App_Auto_Init();
LOG_I("PWR", "periph ready");
/* App 协议走 UART4须在开机后即解析不能等到触摸选模式才 StartTask */
StartTask();
LOG_I("PWR", "periph ready + BT UART recv started");
}
static void PowerOn(void)
@ -226,20 +229,23 @@ static void PowerOff(void)
LCD_FillByColor(0, 0, 240, 320, BLACK);
LCD_BLK_Clr();
LCD_WR_REG(0x28); /* Display OFF */
app_tm1629_all_off();
app_tm1617_off();
/* 关闭电源硬件MCU 保持运行以便长按开机 / 关机充电画面 */
/* 关掉主电源/音源/功放/蓝牙MCU 仍跑以便长按开机与 Type-C 检测 */
BSP_MainPowerEnable(0);
BSP_DreamCorePowerEnable(0);
BSP_HT7178PowerEnable(0);
/* BT 最后关:给已排队的 UART4 ACK如 05 00留出模组转发窗口 */
rt_thread_mdelay(30);
BSP_BlueToothPowerEnable(0);
CurrUIProcress = IdleProcess;
MainTask_Sendmsg(MSG_ID_POWER_OFF, 0, 0, 0);
LOG_I("PWR", "power_off done (soft-off, no reset)");
LOG_I("PWR", "power_off done (soft-off)");
/* 关机时若已插着 Type-C立刻进充电画面 */
/* Type-C 有供电:维持现有逻辑,显示 Charging ImagePOWOFF_CHARG */
{
uint8_t i;
for (i = 0; i < 4; i++)
@ -253,12 +259,29 @@ static void PowerOff(void)
{
usb_charging_state = 1;
usb_charging_laststate = 1;
LOG_I("PWR", "soft-off with USB → charge UI");
LOG_I("PWR", "soft-off with Type-C → charge UI");
MainTask_Sendmsg(MSG_ID_POWOFF_CHARG, percentage, 1, 0);
}
}
}
/* App remote power-off request (BLE cmd 05 00): executed in main-loop context,
so StopFullTask() never detaches the calling BT thread itself. */
static volatile uint8_t s_sys_poweroff_req = 0;
void System_RequestPowerOff(void)
{
s_sys_poweroff_req = 1;
}
void System_PowerOff_Poll(void)
{
if (s_sys_poweroff_req)
{
s_sys_poweroff_req = 0;
if (powon)
PowerOff();
}
}
void Power_Key_Scan()
{
flag_status status = BSP_GetPowerKey();
@ -437,6 +460,16 @@ void Send_volume(uint8_t vol)
SendMidiDataToDreamDSP(mMasterSysExVolume,12);
}
/* 重发当前主音量:开机补发 / 进模式主页 / 开始播放前调用。
* Dream SysEx
* "图标低音量、实际音量大" */
void MasterVolume_Reapply(void)
{
uint8_t vol = app_adc_get_volume();
app_adc_sync_volume(vol); /* 兜底刷新 level保证图标与下发值一致 */
Send_volume(vol);
}
void Accomp_UpdatePlayingChord(void)
{
uint8_t midi_note_buff[4] = {0,};
@ -471,6 +504,8 @@ void ADC_IN1_KEY_Handle(uint8_t key,bool on)
case 1: AutoBandTop1_Postamble(0); break;
case 2: AutoBandTop1_Postamble(1); break;
}
/* 前奏/尾奏起奏前补发主音量,保证与图标一致 */
MasterVolume_Reapply();
//AutoBandTop1_Start();
AutoBandTop1_SyncStart();
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);
Return_Light_buff(KEY_ID_1629);
/* 首次起奏前补发主音量:防开机 SysEx 丢失后"图标低、实际大" */
if (StartFlag == 0)
MasterVolume_Reapply();
switch(mGuiData[GUI_TAB_INDEX].Current)
{
case 3:
@ -760,15 +799,10 @@ void Pick_Handle()
AutoBandTop1_SetLoopMode(0,1);
break;
case 0:
if(PressFlag)
{
AutoBandTop1_SetLoopMode(0,1);
/* 设置页内沿用万能:与主路径一致,始终 loop */
AutoBandTop1_SetPiecePlayMode(1, 0, 480);
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
}else
{
AutoBandTop1_SetPiecePlayMode(1,0,480);
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
}
AutoBandTop1_SetLoopMode(0, 1);
StartFlag = 1;
break;
}
@ -819,17 +853,14 @@ void Pick_Handle()
}
break;
case 0:
/* 万能:和弦走向用 piece 模式;须 SetLoopMode否则无循环时常听不到伴奏 */
StartFlag = 1;
if(PressFlag)
{
AutoBandTop1_SetPiecePlayMode(1,0,480);
AutoBandTop1_SetPiecePlayMode(1, 0, 480);
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
AutoBandTop1_SetLoopMode(0,1);
}else
{
AutoBandTop1_SetPiecePlayMode(1,0,480);
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
}
AutoBandTop1_SetLoopMode(0, 1);
LOG_I("pick", "universal piece name=%s press=%u notes=%u chord=%u",
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
(unsigned)PressFlag, (unsigned)note_cnt, (unsigned)chord_id);
break;
}
}
@ -941,9 +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)
{
//char Info[10];
//sprintf (Info, "%d %d", pos_offset,pitch_offset);
//LCD_ShowString(2, 156+16, (const uint8_t*)Info, RED, WHITE, 16, 0);
if (pos_offset < 1u || pos_offset > 21u)
return;
chord_type_index_map[pos_offset].PitchOffset = pitch_offset;
}
@ -952,10 +982,13 @@ void BT_Chord_offset_map(uint8_t pos_offset,uint8_t chord_offset)
//char Info[20];
STRING_MIDI *MIDI;
const STRING_MIDI *ORGAN_MIDI;
uint8_t group = (pos_offset - 1) / 3; /* 1~3→0, 4~6→1, ..., 19~21→6 */
uint8_t group;
uint8_t chord_APP_Local_offset = 0;
if (pos_offset < 1u || pos_offset > 21u)
return;
group = (pos_offset - 1) / 3; /* 1~3→0, 4~6→1, ..., 19~21→6 */
switch(chord_offset){
case 0x00:
chord_APP_Local_offset = 0;
@ -1280,9 +1313,10 @@ uint8_t GetChordNotesByType(uint8_t MIDI_Index,uint8_t type,uint8_t *buff)
root = buff[0];
for (i = 1; i < n; i++)
{
while (buff[i] >= (uint8_t)(root + 12))
uint8_t guard;
for (guard = 0; guard < 2 && buff[i] >= (uint8_t)(root + 12); guard++)
buff[i] -= 12;
while (buff[i] < root)
for (guard = 0; guard < 2 && buff[i] < root; guard++)
buff[i] += 12;
}
}

View File

@ -168,6 +168,8 @@ extern bool powon;
void Power_Key_Scan(void);
void AutoPowerOff_Scan(void);
void System_PowerOn(void);
void System_RequestPowerOff(void);
void System_PowerOff_Poll(void);
extern uint8_t Led;
extern bool PressFlag;
@ -211,6 +213,7 @@ uint8_t GUI_Item_AjustValue(GUI_SWITCH * Group, int8_t Dir);
void GUI_Item_AjustLoopValue(GUI_SWITCH * Group, int8_t Dir);
void Send_volume(uint8_t vol);
void MasterVolume_Reapply(void);
void ADC_IN1_KEY_Handle(uint8_t key,bool on);
void TM1617_Handle(uint8_t key);

View File

@ -19,10 +19,318 @@ static uint8_t pStoreBuffer[PRESET_BUFFER_SIZE];
//static uint8_t* pStoreBuffer;
#define DEFAULT_TEMPO 100
/* ======== Bass/和弦分通道八度(无 AutoBand 源码时的输出侧映射) ========
* 0 MIDI status 4 8=bass9==
* ch8=bassI/II1~6III+bass -12 E1
* -12 E2~#G4#F(6) -12
* Doc/.xlsx */
#define CHORD_REG_FIX_EN 1
#define BASS_CH 8 /* 日志 ch8 */
#define DRUM_CH 9 /* 日志 ch9 */
#define MIDI_E1 28
#define MIDI_E2 40
#define MIDI_GSHARP4 68
#define XPOSE_FS 6 /* C=0 时 #F=6 */
/* 映射策略指纹:变化时 CC123 清旧音,防挂音(不建大音符表) */
static uint8_t s_reg_fp = 0xFFu;
/* 每个指纹周期内 NoteOn 映射日志限额(和弦与 bass 分开,避免和弦占满看不到 bass */
static uint8_t s_pitch_log_left = 0;
static uint8_t s_bass_log_left = 0;
static uint16_t s_ch_ever_mask = 0; /* 会话内各通道是否出现过 NoteOn */
#define PITCH_LOG_PER_FP 24
#define BASS_LOG_PER_FP 16
static int App_Auto_ClampMidi(int n)
{
if (n < 0) return 0;
if (n > 127) return 127;
return n;
}
static int App_Auto_ChordDegree(void)
{
if (KEY_ID_1629 < 1 || KEY_ID_1629 > 21)
return 0;
return (int)((KEY_ID_1629 - 1) / 3) + 1; /* 1=I .. 7=VII */
}
static int App_Auto_NeedRegisterFix(void)
{
/* 键 1~6 = I/II不动7~21 = III~VII */
return (KEY_ID_1629 >= 7 && KEY_ID_1629 <= 21) ? 1 : 0;
}
static int App_Auto_XposeExtra(void)
{
return (mGuiData[GUI_TRANSPOSE].Current >= XPOSE_FS) ? 1 : 0;
}
static uint8_t App_Auto_RegFingerprint(void)
{
/* bit0=need_fix, bit1=xpose_extra, bit2.. = 粗粒度级数 (key/3) */
uint8_t fp = 0;
if (App_Auto_NeedRegisterFix()) fp |= 0x01u;
if (App_Auto_XposeExtra()) fp |= 0x02u;
if (KEY_ID_1629 >= 1 && KEY_ID_1629 <= 21)
fp |= (uint8_t)(((KEY_ID_1629 - 1) / 3) << 2);
return fp;
}
static void App_Auto_QueueAllNotesOffMelodic(void)
{
MidiFifoItem_t m;
int ch;
for (ch = 0; ch < 16; ch++)
{
if (ch == DRUM_CH) continue;
m.msg[0] = (uint8_t)(0xB0 | ch);
m.msg[1] = 123; /* All Notes Off */
m.msg[2] = 0;
m.msglen = 3;
mymidififo_InQueue(&m_MidiSendFifo, &m);
}
}
/* 超出 #G4在当前和弦内音GetChordNotesByType对照和弦表「和弦伴奏」列中就近 */
static int App_Auto_NearestChordTone(int target)
{
uint8_t buff[4] = {0, 0, 0, 0};
uint8_t n;
uint8_t pcs[4];
int i, oct, best = -1, best_dist = 9999;
uint8_t type;
if (KEY_ID_1629 < 1 || KEY_ID_1629 > 21)
return App_Auto_ClampMidi(target);
type = chord_type_index_map[KEY_ID_1629].type;
n = GetChordNotesByType(KEY_ID_1629, type, buff);
if (n == 0)
return App_Auto_ClampMidi(target > MIDI_GSHARP4 ? MIDI_GSHARP4 : target);
for (i = 0; i < (int)n; i++)
pcs[i] = (uint8_t)(buff[i] % 12);
for (i = 0; i < (int)n; i++)
{
for (oct = 0; oct < 11; oct++)
{
int cand = (int)pcs[i] + oct * 12;
int dist;
if (cand < MIDI_E2 || cand > MIDI_GSHARP4)
continue;
dist = cand - target;
if (dist < 0) dist = -dist;
/* 并列:优先较低者(不超过上限) */
if (dist < best_dist || (dist == best_dist && (best < 0 || cand < best)))
{
best_dist = dist;
best = cand;
}
}
}
if (best < 0)
return MIDI_GSHARP4;
return best;
}
static int App_Auto_MapBassNote(int key, uint8_t *flags)
{
int out = key - 12;
uint8_t f = 0x01u; /* bit0: -12 applied */
while (out < MIDI_E1)
{
out += 12;
f |= 0x02u; /* bit1: floored up to E1 */
}
if (App_Auto_XposeExtra())
{
out -= 12;
f |= 0x04u; /* bit2: xpose extra -12 */
while (out < MIDI_E1)
{
out += 12;
f |= 0x02u;
}
}
if (flags) *flags = f;
return App_Auto_ClampMidi(out);
}
static int App_Auto_MapChordNote(int key, uint8_t *flags)
{
int out = key - 12;
uint8_t f = 0x01u; /* bit0: -12 */
while (out < MIDI_E2)
{
out += 12;
f |= 0x08u; /* bit3: raised to E2 */
}
if (out > MIDI_GSHARP4)
{
out = App_Auto_NearestChordTone(out);
f |= 0x10u; /* bit4: nearest chord tone */
}
if (App_Auto_XposeExtra())
{
out -= 12;
f |= 0x04u;
while (out < MIDI_E2)
{
out += 12;
f |= 0x08u;
}
if (out > MIDI_GSHARP4)
{
out = App_Auto_NearestChordTone(out);
f |= 0x10u;
}
}
if (flags) *flags = f;
return App_Auto_ClampMidi(out);
}
static int App_Auto_MapNote(int channel, int key, uint8_t *flags)
{
int out = key;
if (flags) *flags = 0;
#if CHORD_REG_FIX_EN
if (channel == DRUM_CH || key < 0 || key > 127)
return key;
if (!App_Auto_NeedRegisterFix())
return key;
if (channel == BASS_CH)
out = App_Auto_MapBassNote(key, flags);
else
out = App_Auto_MapChordNote(key, flags);
#else
(void)channel;
#endif
return out;
}
static const char *App_Auto_RoleName(int channel)
{
if (channel == DRUM_CH) return "drum";
if (channel == BASS_CH) return "bass";
return "chord";
}
static void App_Auto_LogPitch(int channel, int key, int out, uint8_t flags, int is_on)
{
const char *role;
int deg;
int fix;
int xf;
int pass;
if (channel == DRUM_CH)
return;
if (channel == BASS_CH)
{
if (s_bass_log_left == 0)
return;
}
else if (s_pitch_log_left == 0)
{
return;
}
role = App_Auto_RoleName(channel);
deg = App_Auto_ChordDegree();
fix = App_Auto_NeedRegisterFix();
xf = App_Auto_XposeExtra();
pass = (!fix || channel == DRUM_CH) ? 1 : 0;
LOG_I("PITCH", "%s ch%u(%s) %d->%d d=%d deg=%d chord=%u xp=%u xf=%d fl=0x%02X %s%s%s%s",
is_on ? "on" : "off",
(unsigned)channel, role, /* 0 基,与 BASS_CH=8 一致 */
key, out, out - key,
deg, (unsigned)KEY_ID_1629,
(unsigned)mGuiData[GUI_TRANSPOSE].Current, xf,
(unsigned)flags,
pass ? "PASS " : "",
(flags & 0x10u) ? "NEAR " : "",
(flags & 0x02u) ? "E1UP " : "",
(flags & 0x08u) ? "E2UP " : "");
if (channel == BASS_CH)
{
if (s_bass_log_left > 0)
s_bass_log_left--;
}
else if (s_pitch_log_left > 0)
{
s_pitch_log_left--;
}
}
/* NoteOn 路径:策略变化时清旧音;返回映射后音高 */
static int App_Auto_MapNoteOn(int channel, int key)
{
#if CHORD_REG_FIX_EN
uint8_t fp = App_Auto_RegFingerprint();
uint8_t flags = 0;
int out;
if (fp != s_reg_fp)
{
if (s_reg_fp != 0xFFu)
{
LOG_I("REG", "fp %u->%u chord=%u deg=%d xp=%u xf=%d",
(unsigned)s_reg_fp, (unsigned)fp,
(unsigned)KEY_ID_1629,
App_Auto_ChordDegree(),
(unsigned)mGuiData[GUI_TRANSPOSE].Current,
App_Auto_XposeExtra());
App_Auto_QueueAllNotesOffMelodic();
}
else
{
LOG_I("REG", "fp init %u chord=%u deg=%d xp=%u",
(unsigned)fp, (unsigned)KEY_ID_1629,
App_Auto_ChordDegree(),
(unsigned)mGuiData[GUI_TRANSPOSE].Current);
}
s_reg_fp = fp;
s_pitch_log_left = PITCH_LOG_PER_FP;
s_bass_log_left = BASS_LOG_PER_FP;
s_ch_ever_mask = 0; /* 新策略周期重新报到通道 */
}
out = App_Auto_MapNote(channel, key, &flags);
if (channel >= 0 && channel < 16)
{
uint16_t bit = (uint16_t)(1u << channel);
if ((s_ch_ever_mask & bit) == 0u)
{
s_ch_ever_mask |= bit;
LOG_I("REG", "ch-seen ch%u role=%s key=%d",
(unsigned)channel, App_Auto_RoleName(channel), key);
}
}
App_Auto_LogPitch(channel, key, out, flags, 1);
return out;
#else
(void)channel;
return key;
#endif
}
static void Func_CallBack_ProgramChange(int channel, int program)
{
MidiFifoItem_t midimsg;
midimsg.msg[0] = 0xc0 | channel;
LOG_I("REG", "pc ch%u prog=%d", (unsigned)channel, program);
midimsg.msg[0] = 0xc0 | (channel & 0x0F);
midimsg.msg[1] = program;
midimsg.msg[2] = 0;
midimsg.msglen = 2;
@ -32,8 +340,10 @@ static void Func_CallBack_ProgramChange(int channel, int program)
static void Func_CallBack_NoteOff(int channel,int key)
{
MidiFifoItem_t midimsg;
midimsg.msg[0] = 0x80 | channel;
midimsg.msg[1] = key;
uint8_t flags = 0;
int out_key = App_Auto_MapNote(channel, key, &flags);
midimsg.msg[0] = 0x80 | (channel & 0x0F);
midimsg.msg[1] = (uint8_t)out_key;
midimsg.msg[2] = 0;
midimsg.msglen = 3;
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
@ -43,9 +353,19 @@ static void Func_CallBack_NoteOff(int channel,int key)
static void Func_CallBack_NoteOn(int channel,int key,int vel)
{
MidiFifoItem_t midimsg;
midimsg.msg[0] = 0x90 | channel;
midimsg.msg[1] = key;
midimsg.msg[2] = vel;
int out_key;
if (vel == 0)
{
uint8_t flags = 0;
out_key = App_Auto_MapNote(channel, key, &flags); /* 视同 NoteOff */
}
else
out_key = App_Auto_MapNoteOn(channel, key);
midimsg.msg[0] = 0x90 | (channel & 0x0F);
midimsg.msg[1] = (uint8_t)out_key;
midimsg.msg[2] = (uint8_t)vel;
midimsg.msglen = 3;
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
@ -135,6 +455,9 @@ static int Func_CallBack_ReadFlash(int address,int length,uint8_t* pOutput)
//
void App_Auto_Init(void)
{
LOG_I("REG", "ch map bass=%d drum=%d fix=%d E1=%d E2=%d Gs4=%d",
BASS_CH, DRUM_CH, CHORD_REG_FIX_EN, MIDI_E1, MIDI_E2, MIDI_GSHARP4);
LOG_I("REG", "PITCH log: chN(role) in->out d=delta deg=I..VII xp=xpose xf=#Fextra fl=flags");
AutoBandTop1_SetPresetStoreBuffer(pStoreBuffer,PRESET_BUFFER_SIZE);
AutoBandTop1_Init();
AutoBandTop1_RegisterCallBack_NoteOn(Func_CallBack_NoteOn);
@ -148,8 +471,12 @@ void App_Auto_Init(void)
int ret = AutoBandTop1_LoadPresetItemFromFlash(0);
if (ret != 0)
{
LOG_E("AUTO", "LoadPreset fail ret=%d addr=0x%08X map=%s",
ret, (unsigned)ADDRESS, ToneFlashMapBank((uint32_t)ADDRESS));
uint8_t hdr[16];
W25Q128_Read(hdr, (uint32_t)ADDRESS, 16);
LOG_E("AUTO", "LoadPreset fail ret=%d addr=0x%08X map=%s magic=%02X%02X%02X%02X cnt=%lu",
ret, (unsigned)ADDRESS, ToneFlashMapBank((uint32_t)ADDRESS),
hdr[0], hdr[1], hdr[2], hdr[3],
(unsigned long)(hdr[12] | (hdr[13] << 8) | (hdr[14] << 16) | (hdr[15] << 24)));
}
else
{

View File

@ -43,12 +43,6 @@ static void UI_ModeSelect_DrawButtons(void)
LCD_WR_PIC_FROM_FLASH(UI0902_MODE_ROW_X, y,
UI0902_MODE_ROW_W, UI0902_MODE_ROW_H,
s_mode_row_addr[i]);
/* 万能模式素材顶部有残留近白横线(相对行内 y=7/8盖成行底色 */
if (i == 0) {
LCD_FillByColor(UI0902_MODE_ROW_X, (uint16_t)(y + 7),
(uint16_t)(UI0902_MODE_ROW_X + UI0902_MODE_ROW_W),
(uint16_t)(y + 9), UI0902_ROW_BG);
}
}
}
@ -169,18 +163,19 @@ void IdleProcess(DisplayTaskMessage_Type msg)
TM1629D_UpdateDisplay(0);
StartTouchTask();
StartScanTask();
/* Dream 上电稍后可能忽略首包 SysEx再发主音量 + 调音台(保持关机前设置) */
/* Dream 上电稍后可能忽略首包 SysEx再发主音量 + 调音台(保持关机前设置)。
* ADC
* MasterVolume_Reapply */
rt_thread_mdelay(400);
vol = ADC_ReadChannel(ADC_CHANNEL_1)>>5;
app_adc_sync_volume(vol);
Send_volume(vol);
MasterVolume_Reapply();
LoadConfig();
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, level, 1, WHITE, GRAY);
break;
case MSG_ID_POWER_OFF:
/* 关机:熄屏 + 关背光 */
/* 无 Type-C整机关机熄屏有 Type-C 时随后 POWOFF_CHARG 会再亮充电图 */
LCD_BLK_Clr();
LCD_WR_REG(0x28); /* Display OFF */
LCD_FillByColor(0, 0, 240, 320, BLACK);
break;
@ -190,6 +185,8 @@ void IdleProcess(DisplayTaskMessage_Type msg)
case MSG_ID_TOUCH:
{
if (!powon)
break; /* 软关机已停触摸;兜底勿进模式选择 */
static uint32_t s_last_enter_ms;
uint32_t now = rt_tick_get() * 1000U / RT_TICK_PER_SECOND;
uint8_t hit = 0U;

View File

@ -53,8 +53,33 @@ void DrawAllFaders(void)
drv_nvm_save_to_flash();
}
/* 0902-06实心蓝色圆钮勿用同心空心圆会透出色条 */
#define MIXER_KNOB_R 14
/* 0902-06圆钮定位半径 = 色条半宽;填充略大以盖住柱端抗锯齿白边(夹紧不画出柱外) */
#define MIXER_KNOB_R 15
#define MIXER_KNOB_FILL_R 16
#define MIXER_TRACK_HALF_W 15
/* 与柱端半圆同形的实心圆钮:顶/底档位与色条圆角完美贴合,无虚线白边 */
static void DrawMixerKnob(uint16_t cx, uint16_t cy)
{
int16_t dy;
int32_t r2 = (int32_t)MIXER_KNOB_FILL_R * (int32_t)MIXER_KNOB_FILL_R;
for (dy = -(int16_t)MIXER_KNOB_R; dy <= (int16_t)MIXER_KNOB_R; dy++)
{
int32_t xx = r2 - (int32_t)dy * (int32_t)dy;
int16_t dx = 0;
int16_t half;
if (xx < 0)
continue;
while ((int32_t)(dx + 1) * (dx + 1) <= xx)
dx++;
half = dx;
if (half > (int16_t)MIXER_TRACK_HALF_W)
half = (int16_t)MIXER_TRACK_HALF_W;
LCD_FillByColor((uint16_t)(cx - half), (uint16_t)(cy + dy),
(uint16_t)(cx + half + 1), (uint16_t)(cy + dy + 1),
COLOR_GRAD_MID);
}
}
static void DrawFaderSlider(Fader_t *f, uint16_t slider_y)
{
@ -62,9 +87,9 @@ static void DrawFaderSlider(Fader_t *f, uint16_t slider_y)
uint16_t ly = f->y + f->h + 8;
uint16_t cy = (uint16_t)(slider_y + MIXER_KNOB_R);
char buf[8];
LCD_FillCircle(cx, cy, MIXER_KNOB_R, COLOR_GRAD_MID);
DrawMixerKnob(cx, cy);
sprintf(buf, "%d", f->val);
LCD_ShowString_AutoAlign((uint16_t)(cx - 8), (uint16_t)(cx + 8),
LCD_ShowString_AutoAlign((uint16_t)(cx - 10), (uint16_t)(cx + 10),
(uint16_t)(cy - 5),
(uint8_t*)buf, LCD_ALIGN_CENTER, WHITE, COLOR_GRAD_MID, 0, 11);
LCD_FillByColor(cx - 15, ly + 17, cx + 15, ly + 19, UI0902_BG_COLOR);
@ -149,17 +174,14 @@ void UpdateFaderByTouch(int idx, uint16_t y)
}
// ============================================================
// ??????Y????????val? 0~10??
// 滑块顶边 Yval0/10 时圆心落在色条底/顶半圆中心,两端对称贴合
// ============================================================
static uint16_t CalcSliderY(Fader_t *f)
{
uint16_t slider_h = (uint16_t)(MIXER_KNOB_R * 2);
uint16_t fill_h = (f->val * f->h) / 10;
if (fill_h > f->h) fill_h = f->h;
uint16_t slider_y = f->y + f->h - fill_h - (slider_h / 2);
if (slider_y < f->y) slider_y = f->y;
if (slider_y > f->y + f->h - slider_h) slider_y = f->y + f->h - slider_h;
return slider_y;
uint16_t top_cy = (uint16_t)(f->y + MIXER_KNOB_R);
uint16_t bot_cy = (uint16_t)(f->y + f->h - 1 - MIXER_KNOB_R);
uint16_t cy = (uint16_t)(bot_cy - ((uint32_t)(bot_cy - top_cy) * f->val) / 10);
return (uint16_t)(cy - MIXER_KNOB_R);
}
int CheckTouchFader(uint16_t x, uint16_t y)
@ -222,7 +244,7 @@ void UI_Mixer_Process(DisplayTaskMessage_Type msg)
break;
case MSG_ID_EC_VOL:
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, msg.HiByte, 1, WHITE, GRAY);
/* 调音台子页无状态栏音量条(与返回键重叠);主音量已在 MainTask 下发 */
ResetAutoPowerCount();
break;
}

View File

@ -243,6 +243,8 @@ void Touch_Action(int8_t FLAG, uint8_t ID, uint8_t areaIndex,uint8_t value)
AutoBandTop1_SetRunVar(mGuiData[ID].Current);
}else
{
/* 触摸起奏前补发主音量,保证实际音量与图标一致 */
MasterVolume_Reapply();
AutoBandTop1_FillinWithIndex(value);
AutoBandTop1_Start();
}

View File

@ -11,11 +11,11 @@ const Touch_AreaTypeDef Touch_SetingSelect_Areas[] = {
{ UI0902_HUB_CARD_X, (uint16_t)(UI0902_HUB_CARD_X + UI0902_HUB_CARD_W - 1),
UI0902_HUB_CARD_Y1, (uint16_t)(UI0902_HUB_CARD_Y1 + UI0902_HUB_CARD_H - 1),
0, 0, GUI_SETING_SELECT3, NULL },
/* 底栏四键 */
{ 0, 69, 280, 319, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
{ 70, 129, 280, 319, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
{ 130, 184, 280, 319, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
{ 185, 239, 280, 319, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
/* 底栏四键:与 Touch_Areas 共用 UI0902_NAV_* 几何 */
{ UI0902_NAV_X0(0), UI0902_NAV_X1(0), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
{ UI0902_NAV_X0(1), UI0902_NAV_X1(1), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
{ UI0902_NAV_X0(2), UI0902_NAV_X1(2), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
{ UI0902_NAV_X0(3), UI0902_NAV_X1(3), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
};
#define TOUCH_AREA_SETING_SELECT_NUM (sizeof(Touch_SetingSelect_Areas)/sizeof(Touch_AreaTypeDef))
@ -124,6 +124,11 @@ void UI_SystemSet_Process(DisplayTaskMessage_Type msg)
label_top();
break;
case MSG_ID_EC_VOL:
/* 主音量已在 MainTask 下发;此处只刷新状态栏音量条,保证与旋钮一致 */
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, msg.HiByte, 1, WHITE, GRAY);
break;
default:
break;
}

View File

@ -54,6 +54,7 @@ void CallUI_Idle(void) {
void CallUI_SongMode(void) {
app_touch_suppress(500);
MasterVolume_Reapply(); /* 进模式补发主音量,防开机 SysEx 丢失后实际音量与图标不符 */
UI_SongMode_Init();
CurrUIProcress = UI_SongMode_Process;
app_log_set_ui_page("Song");
@ -62,6 +63,7 @@ void CallUI_SongMode(void) {
void CallUI_ExpertMode(void) {
app_touch_suppress(500);
MasterVolume_Reapply();
UI_ExpertMode_Init();
// CurrUIProcress = UI_ExpertMode_Process;
CurrUIProcress = UI_SongMode_Process;
@ -71,6 +73,7 @@ void CallUI_ExpertMode(void) {
void CallUI_FreeMode(void) {
app_touch_suppress(500);
MasterVolume_Reapply();
UI_FreeMode_Init();
// CurrUIProcress = UI_FreeMode_Process;
CurrUIProcress = UI_SongMode_Process;
@ -168,10 +171,23 @@ void UI_ReloadTonePreset(void)
if (mGuiData[GUI_TAB_INDEX].Current == 0)
{
int ret = AutoBandTop1_LoadPresetItemFromFlash(ParamGuiData[ALL_MODE_PARAM].Current);
if (ret != 0)
{
uint8_t hdr[16];
W25Q128_Read(hdr, (uint32_t)ADDRESS, 16);
LOG_E("tone", "load universal idx=%d ret=%d magic=%02X%02X%02X%02X cnt=%lu addr=0x%08X",
ParamGuiData[ALL_MODE_PARAM].Current, ret,
hdr[0], hdr[1], hdr[2], hdr[3],
(unsigned long)(hdr[12] | (hdr[13] << 8) | (hdr[14] << 16) | (hdr[15] << 24)),
(unsigned)ADDRESS);
}
else
{
LOG_I("tone", "load universal idx=%d ret=%d name=%s addr=0x%08X map=BIN3@0x%08X",
ParamGuiData[ALL_MODE_PARAM].Current, ret,
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
(unsigned)ADDRESS, (unsigned)EXTFLASH_BIN3_UNIVERSAL_ADDR);
}
return;
}
/* 普通/专业:本地曲目或节奏类型 */
@ -443,6 +459,58 @@ void Show_Battery_Icon(uint8_t flag)
}
/* 中英文混排像素宽(与 LCD_GetMixedStringWidth 规则一致CJK=sizeyASCII=UI0902_ASCII_W */
static uint16_t UI_MixedTextWidth(const uint8_t *str, uint8_t sizey)
{
uint16_t width = 0;
uint8_t ascii_w = UI0902_ASCII_W(sizey);
if (str == NULL)
return 0;
while (*str != '\0') {
if (*str > 0x80) {
width = (uint16_t)(width + sizey);
str += 2;
} else {
width = (uint16_t)(width + ascii_w);
str += 1;
}
}
return width;
}
/* 曲目/音色共用序号与名称间距与「1.名称」拼接一致,保证两行视觉间距相同) */
#define UI0902_INDEX_NAME_GAP 0
/* 在 [x1,x2] 内整体居中绘制「N.」+ 名称无渐变show_Param 用) */
static void UI_DrawIndexedName_Mixed(uint16_t x1, uint16_t x2, uint16_t text_y,
const char *num, const char *name,
uint16_t fc, uint16_t bc, uint8_t sizey)
{
char line[48] = {0};
snprintf(line, sizeof(line), "%s%s", num, name);
LCD_ShowMixedString(x1, x2, text_y, (uint8_t *)line, LCD_ALIGN_CENTER,
fc, bc, sizey, 1);
}
/* 在 [x1,x2] 内整体居中绘制「N.」+ 中文名(渐变底,音色行用);间隙与曲目行一致 */
static void UI_DrawIndexedName_Grad(uint16_t x1, uint16_t x2, uint16_t text_y,
const char *num, const uint8_t *name,
uint16_t fc, uint16_t c_top, uint16_t c_bot, uint8_t sizey)
{
uint16_t num_w = (uint16_t)(strlen(num) * UI0902_ASCII_W(sizey));
uint16_t name_w = UI_MixedTextWidth(name, sizey);
uint16_t total_w = (uint16_t)(num_w + UI0902_INDEX_NAME_GAP + name_w);
uint16_t area_w = (x2 > x1) ? (uint16_t)(x2 - x1) : 0;
uint16_t start_x = (total_w >= area_w) ? x1 : (uint16_t)(x1 + (area_w - total_w) / 2);
uint16_t name_x = (uint16_t)(start_x + num_w + UI0902_INDEX_NAME_GAP);
LCD_ShowString_AutoAlign_Gradient(start_x, (uint16_t)(start_x + num_w), text_y,
(const uint8_t *)num, LCD_ALIGN_LEFT, fc, c_top, c_bot, sizey);
LCD_ShowChinese_AutoAlign_Gradient(name_x, x2, text_y, name, LCD_ALIGN_LEFT,
fc, c_top, c_bot, sizey);
}
void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
{
char Textstr_new[40] = {0};
@ -453,9 +521,19 @@ void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
const uint16_t text_y = row_y + (row_h - UI->Text_size) / 2 + 1;
uint8_t idx = Group->Current + 1;
uint16_t c_top, c_bot;
uint16_t x1, x2;
UI_GetFocusGrad(GUI_MODE_PARAM, &c_top, &c_bot);
x1 = (uint16_t)(UI->x + 10);
x2 = (uint16_t)(UI->x + UI->w - 10);
/* 修复曲目名尾部残留旧名比新名长时只填渐变胶囊x+25 ~ x+w-40
x+10 ~ x+w-10
*/
LCD_FillByColor(x1, row_y, x2, (uint16_t)(row_y + row_h), c_top);
LCD_FillRoundRectGradient(
UI->x+25, row_y,
UI->x + UI->w-40-(UI->x+25),
@ -464,18 +542,21 @@ void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
c_top, c_bot
);
// 拼接序号 1. 2. 3.
/* 序号+名称整体居中约等于标注「右移7px」的视觉效果 */
sprintf(NumBuf, "%d.", idx);
uint16_t num_w = (uint16_t)(strlen(NumBuf) * UI0902_ASCII_W(UI->Text_size));
if (Group->Id == EXPRESS_MODE_PARAM && Group->Current < LOCAL_SONG_COUNT)
strcpy(Textstr_new, LocalSongNameGbk[Group->Current]);
else
GET_ParamNum_Str(Group->Current, 0, Textstr_new, Group->Id);
uint32_t string_start_x = LCD_ShowMixedString(UI->x+10, UI->x + UI->w, text_y, (uint8_t *)Textstr_new , LCD_ALIGN_CENTER, WHITE, BLACK, UI->Text_size, 1);
//序号X = 中文起点 - 数字宽度 - 5<EFBC8C>?像素
uint16_t num_x = string_start_x - num_w;
// 绘制左侧数字序号
LCD_ShowString(num_x, text_y, (const uint8_t *)NumBuf, UI->Text_fc, UI->Text_bc, UI->Text_size, 1);
UI_DrawIndexedName_Mixed(x1, x2, text_y, NumBuf, Textstr_new,
WHITE, BLACK, UI->Text_size);
/* 补画左右三角(几何与 label_ModeSelece_ui 一致) */
{
uint16_t row_cy = (uint16_t)(row_y + row_h / 2);
UI_DrawTriLeft((uint16_t)(UI->x + 5), row_cy);
UI_DrawTriRight((uint16_t)(UI->x + UI->w - 5 - UI0902_TRI_R_W), row_cy);
}
}
@ -803,12 +884,8 @@ void label_ModeSelece_ui(const LEBEL_UI *UI,GUI_SWITCH * Group)
paramCurData = &ParamGuiData[SONG_MODE_PARAM];
}
/* 左右三角由 show_Param 末尾统一补画(整行清除后必须重画) */
show_Param(&UI_Label[GUI_MODE_PARAM], paramCurData);
{
uint16_t row_cy = UI->y + UI0902_TAB_H + 4 + (UI->h - UI0902_TAB_H - 4) / 2;
UI_DrawTriLeft(UI->x + 5, row_cy);
UI_DrawTriRight((uint16_t)(UI->x + UI->w - 5 - UI0902_TRI_R_W), row_cy);
}
}
void label_TimbreSelece_ui(const LEBEL_UI *UI, GUI_SWITCH * Group)
@ -851,22 +928,17 @@ void label_TimbreSelece_ui(const LEBEL_UI *UI, GUI_SWITCH * Group)
c_top, c_bot
);
sprintf(NumBuf, "%d.", idx);
uint16_t num_w = (uint16_t)(strlen(NumBuf) * UI0902_ASCII_W(UI->Text_size));
GET_Num_Str(mGuiData[UI->Id].Current, 0, (char*)Textstr, UI->Id);
uint32_t chinese_start_x = LCD_ShowChinese_AutoAlign_Gradient(
UI->x + num_w,
UI->x + UI->w,
UI_DrawIndexedName_Grad(
(uint16_t)(UI->x + 10),
(uint16_t)(UI->x + UI->w - 10),
text_y,
(const uint8_t*)Textstr,
UI->Text_Align,
NumBuf,
Textstr,
UI->Text_fc,
c_top, c_bot,
UI->Text_size
);
uint16_t num_x = chinese_start_x - num_w - 5;
LCD_ShowString_AutoAlign_Gradient(num_x, num_x + num_w, text_y,
(const uint8_t *)NumBuf, LCD_ALIGN_LEFT, UI->Text_fc,
c_top, c_bot, UI->Text_size);
switch(Group->Id)
{
case GUI_TIMBRE_SELECT:
@ -907,37 +979,32 @@ void UI_DrawSubPageHeader(const uint8_t *title_gbk)
void Draw_Bottom_Bar(uint8_t sel, uint8_t first_back)
{
/* 需求稿四键:万能 | 普通 | 专业 | 设置first_back 时设置位仍高亮(返回用触控逻辑) */
/* 四键合成图(图标+原稿文字);统一 32 高,文字底边对齐 */
const uint8_t *icons[4];
uint8_t ws[4], hs[4];
/* 0904四键中心约 40 / 100 / 160 / 210 */
uint8_t ws[4];
const uint16_t cx[4] = { 40, 100, 160, 210 };
const uint8_t icon_h = 32;
const uint16_t icon_y = 282;
uint8_t i;
(void)first_back;
/* 从段落钮底边之下清屏,勿切掉按钮底圆角(钮 y=231..264 */
LCD_FillByColor(0, (uint16_t)(UI0902_SECTION_Y + UI0902_SECTION_BTN_H),
240, 320, UI0902_BG_COLOR);
/* mode.png段落钮与底栏之间横线分隔与顶栏同色同厚 */
LCD_FillByColor(0, UI0902_SEP_LINE_Y, 240,
(uint16_t)(UI0902_SEP_LINE_Y + UI0902_SEP_LINE_THICK),
UI0902_SEP_LINE_COLOR);
if (sel == UI0902_NAV_UNIVERSAL) { icons[0] = gImage_Nav_Universal_Sel; ws[0] = 36; hs[0] = 28; }
else { icons[0] = gImage_Nav_Universal_Not; ws[0] = 36; hs[0] = 28; }
if (sel == UI0902_NAV_NORMAL) { icons[1] = gImage_Nav_Normal_Sel; ws[1] = 35; hs[1] = 29; }
else { icons[1] = gImage_Nav_Normal_Not; ws[1] = 35; hs[1] = 28; }
if (sel == UI0902_NAV_EXPERT) { icons[2] = gImage_Nav_Expert_Sel; ws[2] = 35; hs[2] = 27; }
else { icons[2] = gImage_Nav_Expert_Not; ws[2] = 35; hs[2] = 27; }
if (sel == UI0902_NAV_SETTING) { icons[3] = gImage_Nav_Setting_Sel; ws[3] = 18; hs[3] = 28; }
else { icons[3] = gImage_Nav_Setting_Not; ws[3] = 18; hs[3] = 28; }
icons[0] = (sel == UI0902_NAV_UNIVERSAL) ? gImage_Nav_Universal_Sel : gImage_Nav_Universal_Not;
icons[1] = (sel == UI0902_NAV_NORMAL) ? gImage_Nav_Normal_Sel : gImage_Nav_Normal_Not;
icons[2] = (sel == UI0902_NAV_EXPERT) ? gImage_Nav_Expert_Sel : gImage_Nav_Expert_Not;
icons[3] = (sel == UI0902_NAV_SETTING) ? gImage_Nav_Setting_Sel : gImage_Nav_Setting_Not;
ws[0] = ws[1] = ws[2] = 40;
ws[3] = 22;
for (i = 0; i < 4; i++)
{
LCD_WR_PIC_Trans(cx[i] - ws[i] / 2, 283, ws[i], hs[i], icons[i], BLACK);
LCD_WR_PIC_Trans((uint16_t)(cx[i] - ws[i] / 2), icon_y,
ws[i], icon_h, icons[i], BLACK);
}
}
@ -988,7 +1055,7 @@ void Draw_Tab_Section_Menu(const LEBEL_UI *UI, TAB_SECTION_INDEX select_tab)
const uint8_t pitch = UI0902_SECTION_PITCH;
const uint16_t start_x = UI0902_MARGIN_X; /* 0902-0412/69/126/183 */
const uint16_t start_y = UI->y;
/* 0819演奏段落数字与标题同为 13按钮内垂直居中视觉重心 +1 */
/* 0819演奏段落数字与标题同为 13按钮内垂直居中视觉重心 +1;稿面数字整体右移 2px */
const uint16_t text_y = start_y + (tab_h - UI0902_FONT_BODY) / 2 + 1;
const char *nums[4] = {"1", "2", "3", "4"};
uint8_t i;
@ -999,7 +1066,7 @@ void Draw_Tab_Section_Menu(const LEBEL_UI *UI, TAB_SECTION_INDEX select_tab)
uint16_t bg = (select_tab == i) ? COLOR_GRAD_MID : UI0902_ROW_BG;
LCD_FillRoundRect(x, start_y, tab_w, tab_h, tab_r, bg);
LCD_ShowString_AutoAlign(
x, (uint16_t)(x + tab_w - 1), text_y,
(uint16_t)(x + 2), (uint16_t)(x + tab_w - 1 + 2), text_y,
(const uint8_t*)nums[i],
UI->Text_Align,
WHITE,
@ -1223,11 +1290,11 @@ void Draw_System_Item_List(const LEBEL_UI *UI, GUI_SWITCH *Group)
}
break;
case GUI_BLUETOOTH_SW:
/* 素材:系统设置图标-11白蓝牙符Flash 槽名 RESTORE尺寸 13x21 */
/* 素材:系统设置图标-11白蓝牙符→ ICON_SET_BLUETOOTH 13x21 */
LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12,
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_RESTORE_H) / 2),
UI0902_ICON_SYS_RESTORE_W, UI0902_ICON_SYS_RESTORE_H,
UI0902_ICON_SYS_RESTORE_ADDR);
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SET_BLUETOOTH_H) / 2),
UI0902_ICON_SET_BLUETOOTH_W, UI0902_ICON_SET_BLUETOOTH_H,
UI0902_ICON_SET_BLUETOOTH_ADDR);
UI_DrawTriLeft(val_x1, chev_cy);
if(Group->Current)
LCD_ShowChinese_AutoAlign_Gradient(val_x1 + UI0902_TRI_L_W, val_x2 - UI0902_TRI_R_W, text_y,(const uint8_t*)"\xCA\xC7",LCD_ALIGN_CENTER, WHITE,c_top, c_bot,value_sz);
@ -1264,13 +1331,17 @@ void Draw_System_Item_List(const LEBEL_UI *UI, GUI_SWITCH *Group)
drv_nvm_save_to_flash();
break;
case GUI_RESTORE:
/* 逆时针箭头:当前在 VERSION 槽(19x18)RESTORE 槽曾误为蓝牙符 */
/* 素材:系统设置图标-13逆时针恢复→ ICON_SYS_RESTORE 19x18 */
LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12,
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_VERSION_H) / 2),
UI0902_ICON_SYS_VERSION_W, UI0902_ICON_SYS_VERSION_H,
UI0902_ICON_SYS_VERSION_ADDR);
/* 右三角与蓝牙/自动关机同一右缘val_x2 */
UI_DrawTriRight((uint16_t)(val_x2 - UI0902_TRI_R_W), chev_cy);
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_RESTORE_H) / 2),
UI0902_ICON_SYS_RESTORE_W, UI0902_ICON_SYS_RESTORE_H,
UI0902_ICON_SYS_RESTORE_ADDR);
/* 0902-07右侧为进入下级页的细 chevron设置图标-03勿用实心三角 */
LCD_WR_PIC_FROM_FLASH_Trans(
(uint16_t)(val_x2 - UI0902_ICON_CHEVRON_W),
(uint16_t)(UI->y + (UI->h - UI0902_ICON_CHEVRON_H) / 2),
UI0902_ICON_CHEVRON_W, UI0902_ICON_CHEVRON_H,
UI0902_ICON_CHEVRON_ADDR);
break;
default: break;
}

View File

@ -173,19 +173,25 @@ void CallUI_RestoreSelect(void);
#define UI0902_NAV_EXPERT 2 /* 底栏:专业模式 */
#define UI0902_NAV_SETTING 3 /* 底栏:设置 */
#define UI0902_NAV_NONE 0xFF
/* 底栏触区:段落钮底(y264)之下四等分全宽,各 60×55 */
#define UI0902_NAV_Y0 265
#define UI0902_NAV_Y1 319
#define UI0902_NAV_W 60
#define UI0902_NAV_X0(i) ((uint16_t)((i) * UI0902_NAV_W))
#define UI0902_NAV_X1(i) ((uint16_t)((i) * UI0902_NAV_W + UI0902_NAV_W - 1))
/* 兼容旧名(已废弃三键:设置|调音台|模式) */
#define UI0902_NAV_MIXER UI0902_NAV_NORMAL
#define UI0902_NAV_MODE UI0902_NAV_EXPERT
/* 底部图标实际内容(生成脚本曾把 万能/普通/专业/设置 误命名为 Setting/Mixer/Mode */
#define gImage_Nav_Universal_Not gImage_UI0902_TabSetting_Sel_36x28 /* 底部-10 白 */
#define gImage_Nav_Universal_Sel gImage_UI0902_TabMode_Sel_36x28 /* 底部-14 蓝 */
#define gImage_Nav_Normal_Not gImage_UI0902_TabSetting_Not_35x28 /* 底部-11 白 */
#define gImage_Nav_Normal_Sel gImage_UI0902_TabMode_Not_35x29 /* 底部-15 蓝 */
#define gImage_Nav_Expert_Not gImage_UI0902_TabMixer_Sel_35x27 /* 底部-12 白 */
#define gImage_Nav_Expert_Sel gImage_UI0902_TabBack_Sel_35x27 /* 底部-16 蓝 */
#define gImage_Nav_Setting_Not gImage_UI0902_TabMixer_Not_18x28 /* 底部-13 白 */
#define gImage_Nav_Setting_Sel gImage_UI0902_TabBack_Not_18x28 /* 底部-17 蓝 */
/* 底部图标0909 加大图标 + 原稿文字条(合成图);生成脚本曾误命名 Setting/Mixer/Mode */
#define gImage_Nav_Universal_Not gImage_UI0902_TabSetting_Sel_40x32 /* 底部-10 白 */
#define gImage_Nav_Universal_Sel gImage_UI0902_TabMode_Sel_40x32 /* 底部-14 蓝 */
#define gImage_Nav_Normal_Not gImage_UI0902_TabSetting_Not_40x32 /* 底部-11 白 */
#define gImage_Nav_Normal_Sel gImage_UI0902_TabMode_Not_40x32 /* 底部-15 蓝 */
#define gImage_Nav_Expert_Not gImage_UI0902_TabMixer_Sel_40x32 /* 底部-12 白 */
#define gImage_Nav_Expert_Sel gImage_UI0902_TabBack_Sel_40x32 /* 底部-16 蓝 */
#define gImage_Nav_Setting_Not gImage_UI0902_TabMixer_Not_22x32 /* 底部-13 白 */
#define gImage_Nav_Setting_Sel gImage_UI0902_TabBack_Not_22x32 /* 底部-17 蓝 */
/* 旧几何电池Draw_Battery_Icon最左起点0902 图电池用 Draw_Status_Battery_Icon */
#define UI_STATUS_BATTERY_X 150
#define UI_STATUS_BATTERY_H 11

File diff suppressed because it is too large Load Diff

View File

@ -7,15 +7,15 @@ extern const unsigned char gImage_UI0902_BatteryOutline_29x12[];
extern const unsigned char gImage_UI0902_Volume_20x15[600];
extern const unsigned char gImage_UI0902_Bluetooth_11x17[374];
extern const unsigned char gImage_UI0902_BatteryFill_22x9[396];
extern const unsigned char gImage_UI0902_TabSetting_Sel_36x28[2016];
extern const unsigned char gImage_UI0902_TabSetting_Not_35x28[1960];
extern const unsigned char gImage_UI0902_TabMixer_Sel_35x27[1890];
extern const unsigned char gImage_UI0902_TabMixer_Not_18x28[1008];
extern const unsigned char gImage_UI0902_TabSetting_Sel_40x32[2560];
extern const unsigned char gImage_UI0902_TabSetting_Not_40x32[2560];
extern const unsigned char gImage_UI0902_TabMixer_Sel_40x32[2560];
extern const unsigned char gImage_UI0902_TabMixer_Not_22x32[1408];
extern const unsigned char gImage_UI0902_TabMixer_Not_35x28[1960];
extern const unsigned char gImage_UI0902_TabMode_Sel_36x28[2016];
extern const unsigned char gImage_UI0902_TabMode_Not_35x29[2030];
extern const unsigned char gImage_UI0902_TabBack_Sel_35x27[1890];
extern const unsigned char gImage_UI0902_TabBack_Not_18x28[1008];
extern const unsigned char gImage_UI0902_TabMode_Sel_40x32[2560];
extern const unsigned char gImage_UI0902_TabMode_Not_40x32[2560];
extern const unsigned char gImage_UI0902_TabBack_Sel_40x32[2560];
extern const unsigned char gImage_UI0902_TabBack_Not_22x32[1408];
extern const unsigned char gImage_UI0902_BtnMinus_23x22[1012];
extern const unsigned char gImage_UI0902_BtnPlus_23x22[1012];

View File

@ -1,6 +1,7 @@
// File: w25q128.c
#include "w25q128.h"
#include "wk_system.h"
#include "rtthread.h"
/* W25Q128 挂在 SPI1 上PA5=SCK, PA6=MISO, PA7=MOSICS 用 PA4 GPIO 控制 */
#define W25Q128_SPI SPI1
@ -82,6 +83,12 @@ static void W25Q128_WaitBusy(void)
W25Q128_ReadWriteByte(W25X_ReadStatusReg1); // 发送读状态寄存器命令
do {
status = W25Q128_ReadWriteByte(0xFF); // 循环读取状态字
if (status & 0x01) {
SPI_CS_HIGH();
rt_thread_mdelay(1);
SPI_CS_LOW();
W25Q128_ReadWriteByte(W25X_ReadStatusReg1);
}
} while (status & 0x01); // 等待BUSY位清零
SPI_CS_HIGH();
}

View File

@ -37,3 +37,22 @@ void SendMidiDataToDreamDSP(uint8_t* buff, uint16_t cnt)
{
USART2_SendData(buff,cnt);
}
#define AT32_UID_BASE 0x1FFFF7E8UL
void BleBuildDeviceName(char *out)
{
static const char hex[] = "0123456789ABCDEF";
const uint32_t *uid = (const uint32_t *)AT32_UID_BASE;
uint32_t tail = uid[2] & 0x00FFFFFFu;
unsigned i;
out[0] = 'P';
out[1] = 'H';
out[2] = 'O';
out[3] = 'N';
out[4] = '_';
for (i = 0; i < 6; i++)
out[5 + i] = hex[(tail >> (4 * (5 - i))) & 0xFu];
out[BLE_DEVICE_NAME_LEN] = '\0';
}

View File

@ -11,4 +11,8 @@ void SendVolumeChangeMidiEvent(uint8_t channel, uint8_t CC);
void SendMidiDataToDreamDSP(uint8_t* buff, uint16_t cnt);
void DefaultTask_SendMsg(uint16_t Data1, uint16_t Data2, uint16_t Data3, uint16_t Data4);
/* Unique BLE/BT name from AT32 UID: "PHON_" + 6 hex (last 3 UID bytes). out >= 12. */
#define BLE_DEVICE_NAME_LEN 11
void BleBuildDeviceName(char *out);
#endif /* __MIDI_SEND_H__ */

View File

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

View File

@ -18,7 +18,7 @@
#define EXTFLASH_CHARGING_H 190
#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_SIZE 41263UL

View File

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

View File

@ -105,6 +105,7 @@ int main(void)
/* add user code begin 3 */
Power_Key_Scan();
AutoPowerOff_Scan();
System_PowerOff_Poll();
LCD_Dump_Poll();
#if !DEBUG_LCD_DUMP
app_log_poll();

View File

@ -3,14 +3,14 @@
#define BUFF_SIZE 512
/* 纯比较宏:参数必须是普通局部变量(非 volatile
访 volatile Pa082 */
/* ?????????????????????????????? volatile????
?????????????????? volatile ???? Pa082 ????????? */
#define IS_FULL(head, tail) ((((head) + 1) % BUFF_SIZE) == (tail))
#define IS_EMPTY(head, tail) ((head) == (tail))
/* ==================== 环形队列结构体 ==================== */
/* ==================== ???????????? ==================== */
/* 一个串口实例的环形队列(收发各一) */
/* ????????????????????????????? */
typedef struct
{
uint8_t rx_buff[BUFF_SIZE];
@ -21,14 +21,14 @@ typedef struct
volatile uint16_t tx_head;
volatile uint16_t tx_tail;
usart_type *usart; /* 关联的串口外设USART2 / UART4 */
usart_type *usart; /* ??????????????USART2 / UART4?? */
} uart_ring_t;
/* USART2 和 UART4 各一个实例 */
/* USART2 ?? UART4 ???????? */
static uart_ring_t uart2;
static uart_ring_t uart4;
/* 初始化:关联串口外设(由 wk_usart2_init / wk_uart4_init 调用) */
/* ?????????????????????? wk_usart2_init / wk_uart4_init ????? */
void uart_ring_usart2_init(void)
{
uart2.usart = USART2;
@ -39,19 +39,19 @@ void uart_ring_uart4_init(void)
uart4.usart = UART4;
}
/* ==================== 通用入队/出队(带实例参数) ==================== */
/* ==================== ??????/???????????????? ==================== */
/* 入队ISR 调用)。先快照 volatile 值再操作,避免 Pa082 */
/* ????ISR ???????????? volatile ???????????? Pa082 */
static void ring_rx_push(uart_ring_t *rb, uint8_t c)
{
uint16_t h = rb->rx_head;
uint16_t t = rb->rx_tail;
if(IS_FULL(h, t)) return; /* 满则丢,防止覆盖未读数据 */
if(IS_FULL(h, t)) return; /* ???????????????????? */
rb->rx_buff[h] = c;
rb->rx_head = (h + 1) % BUFF_SIZE;
}
/* 出队(业务线程调用) */
/* ??????????????? */
static uint8_t ring_rx_pop(uart_ring_t *rb, uint8_t *c)
{
uint16_t h = rb->rx_head;
@ -62,65 +62,81 @@ static uint8_t ring_rx_pop(uart_ring_t *rb, uint8_t *c)
return 1;
}
/* 写入 tx 队列(调用前应已关中断,防止 ISR 同时读) */
/* ???? tx ??????????????????????? ISR ?????? */
static void ring_tx_write(uart_ring_t *rb, uint8_t *data, uint16_t len)
{
uint16_t h = rb->tx_head;
uint16_t t = rb->tx_tail;
for(uint16_t i = 0; i < len; i++)
{
if(IS_FULL(h, t)) break; /* 用 tx 判满 */
if(IS_FULL(h, t)) break; /* ?? tx ???? */
rb->tx_buff[h] = data[i];
h = (h + 1) % BUFF_SIZE;
}
rb->tx_head = h; /* 最后一次性写回 volatile */
rb->tx_head = h; /* ???????????? volatile */
}
/* ==================== 发送接口(带串口参数) ==================== */
/* ==================== ?????????????????? ==================== */
/* 通用发送:关中断 → 写队列 → 开 TDBE 中断触发发送 */
/* ?????????????? ?? ?????? ?? ?? TDBE ??????????? */
static void uart_ring_send(uart_ring_t *rb, uint8_t *data, uint16_t len)
{
rt_base_t level = rt_hw_interrupt_disable();
ring_tx_write(rb, data, len);
usart_interrupt_enable(rb->usart, USART_TDBE_INT, TRUE); /* 开发送中断 */
usart_interrupt_enable(rb->usart, USART_TDBE_INT, TRUE); /* ?????????? */
rt_hw_interrupt_enable(level);
}
/* USART2 发送对外midi_send.c 等在用) */
/* USART2 ?????????midi_send.c ??????? */
void USART2_SendData(uint8_t *data, uint16_t len)
{
uart_ring_send(&uart2, data, len);
}
/* UART4 发送(对外) */
/* UART4 ????????? */
void USART4_SendData(uint8_t *data, uint16_t len)
{
uart_ring_send(&uart4, data, len);
}
/* USART2 接收出队(对外) */
void USART4_WaitTxIdle(uint32_t timeout_ms)
{
uint32_t t0 = rt_tick_get();
while ((rt_tick_get() - t0) < rt_tick_from_millisecond(timeout_ms ? timeout_ms : 1))
{
uint16_t h = uart4.tx_head;
uint16_t t = uart4.tx_tail;
if (IS_EMPTY(h, t))
{
/* <20><><EFBFBD><EFBFBD>պ<EFBFBD><D5BA>ٵ<EFBFBD><D9B5><EFBFBD>λ<EFBFBD>Ĵ<EFBFBD><C4B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> */
if (usart_flag_get(uart4.usart, USART_TDC_FLAG) == SET)
return;
}
rt_thread_mdelay(1);
}
}
/* USART2 ???????????? */
uint8_t USART2_RxPop(uint8_t *c)
{
return ring_rx_pop(&uart2, c);
}
/* UART4 接收出队(对外) */
/* UART4 ???????????? */
uint8_t UART4_RxPop(uint8_t *c)
{
return ring_rx_pop(&uart4, c);
}
/* ==================== 串口中断 ==================== */
/* ==================== ???????? ==================== */
static void uart_ring_isr(uart_ring_t *rb)
{
/* 接收:收到字节 → 入 rx 队列 */
if(usart_flag_get(rb->usart, USART_RDBF_FLAG) == SET)
{
uint8_t data = usart_data_receive(rb->usart); /* 读数据(自动清 RDBF */
uint8_t data = usart_data_receive(rb->usart);
ring_rx_push(rb, data);
if(rb->usart == UART4)
{
@ -128,7 +144,6 @@ static void uart_ring_isr(uart_ring_t *rb)
}
}
/* 发送tx 队列有数据 → 发一个字节;发完 → 关 TDBE 中断 */
if(usart_flag_get(rb->usart, USART_TDBE_FLAG) == SET)
{
uint16_t h = rb->tx_head;
@ -140,11 +155,10 @@ static void uart_ring_isr(uart_ring_t *rb)
}
else
{
usart_interrupt_enable(rb->usart, USART_TDBE_INT, FALSE); /* 发完关中断 */
usart_interrupt_enable(rb->usart, USART_TDBE_INT, FALSE);
}
}
/* 错误处理:清溢出标志,防止反复进中断卡死 */
if(usart_flag_get(rb->usart, USART_ROERR_FLAG) == SET)
usart_flag_clear(rb->usart, USART_ROERR_FLAG);
}

View File

@ -1,4 +1,5 @@
#include "includes.h"
#include "git_user_fw_ver.h"
// Э<><D0AD>֡ͷ/֡β
@ -25,7 +26,7 @@ typedef enum
UART4_RCV_BUFF_MIDIEND
} UART4_RCV_StatusType;
#define UART4_PROCESS_BUFF_SIZE 32
#define UART4_PROCESS_BUFF_SIZE 64 /* 02 04 full chord-map frame is 47 bytes */
// UART4<54><34><EFBFBD><EFBFBD>ȫ<EFBFBD>ֱ<EFBFBD><D6B1><EFBFBD>
static UART4_RCV_StatusType UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
@ -48,22 +49,25 @@ typedef struct
// ==============================
// ָ<><EFBFBD><EEB4A6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȫ<EFBFBD><C8AB> static<69><63>
// ==============================
//static void handleDevDisconnect(uint8_t *data);
//static void handleDevConnect(uint8_t *data);
//static void handleDevName(uint8_t *data);
//static void handleFwMainVer(uint8_t *data);
//static void handleSoundVer(uint8_t *data);
//static void handleUIVer(uint8_t *data);
//static void handleOtherInfo(uint8_t *data);
//static void handleDevCode(uint8_t *data);
//static void handleAutoPowerOff(uint8_t *data);
static void handleDevDisconnect(uint8_t *data);
static void handleDevConnect(uint8_t *data);
static void handleDevName(uint8_t *data);
static void handleFwMainVer(uint8_t *data);
static void handleSoundVer(uint8_t *data);
static void handleUIVer(uint8_t *data);
static void handleOtherInfo(uint8_t *data);
static void handleDevCode(uint8_t *data);
static void handleAutoPowerOff(uint8_t *data);
static void handleUserFwVer(uint8_t *data);
static void handleAutoPowerOffSet(uint8_t *data);
static void handleFwCode(uint8_t *data);
static void handleReadRhythmMap(uint8_t *data);
static void handlePitchOffset(uint8_t *data);
static void handleChordOffset(uint8_t *data);
//static void handleResetChordMap(uint8_t *data);
static void handleResetChordMap(uint8_t *data);
//static void handleRhythmStyle(uint8_t *data);
static void handleRhythmStyle(uint8_t *data);
static void handleStringTimbre(uint8_t *data);
static void handleBPM(uint8_t *data);
static void handleTranspose(uint8_t *data);
@ -75,9 +79,8 @@ static void handleLED3(uint8_t *data);
static void handleLED4(uint8_t *data);
static void handleLED5(uint8_t *data);
static void handleLED6(uint8_t *data);
//static void handleStopPlay(uint8_t *data);
//static void handleDeviceReset(uint8_t *data);
static void handleDeviceReset(uint8_t *data);
static void handleIntro(uint8_t *data);
static void handleInterlude(uint8_t *data);
@ -98,52 +101,54 @@ static void handleSectionD(uint8_t *data);
*************************************************/
static const BLE_SysExCmdItem bleSysExCmdTable[] =
{
// //======== 1. <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ 0x01 ========
// {{0x01, 0x00}, handleDevDisconnect}, // <20>Ͽ<EFBFBD><CFBF>
// {{0x01, 0x01}, handleDevConnect}, // <20><><EFBFBD><EFBFBD><EFBFBD>
// {{0x01, 0x02}, handleDevName}, // <20><>ȡ<EFBFBD><EFBFBD><E8B1B8>
// {{0x01, 0x03}, handleFwMainVer}, // <20>̼<EFBFBD><CCBC><EFBFBD><EFBFBD>
// {{0x01, 0x04}, handleSoundVer}, // <20><>Դ<EFBFBD>
// {{0x01, 0x05}, handleUIVer}, // UI<55>
// {{0x01, 0x06}, handleOtherInfo}, // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
// {{0x01, 0x07}, handleDevCode}, // <20><EFBFBD><E8B1B8><EFBFBD><EFBFBD>
// {{0x01, 0x0A}, handleAutoPowerOff}, // <20>Զ<EFBFBD><D4B6>ػ<EFBFBD>
//
// //======== 2. ӳ<><D3B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 0x02 ========
{{0x02, 0x01}, handleReadRhythmMap}, // <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD>/<2F><><EFBFBD><EFBFBD>ӳ<EFBFBD><D3B3><EFBFBD>
{{0x02, 0x02}, handlePitchOffset}, // Pitchƫ<68><C6AB>
{{0x02, 0x03}, handleChordOffset}, // Chordƫ<64><C6AB>
// {{0x02, 0x04}, handleResetChordMap}, // <20><><EFBFBD>ú<EFBFBD><C3BA><EFBFBD>ӳ<EFBFBD><D3B3>Ĭ<EFBFBD><C4AC>ֵ
/*======== 1. device info 0x01 ========*/
{{0x01, 0x00}, handleDevDisconnect}, /* disconnect */
{{0x01, 0x01}, handleDevConnect}, /* connect */
{{0x01, 0x02}, handleDevName}, /* device name */
{{0x01, 0x03}, handleFwMainVer}, /* fw main version */
{{0x01, 0x04}, handleSoundVer}, /* sound version */
{{0x01, 0x05}, handleUIVer}, /* UI version */
{{0x01, 0x06}, handleOtherInfo}, /* other info */
{{0x01, 0x07}, handleDevCode}, /* device code (96bit UID) */
{{0x01, 0x0A}, handleAutoPowerOff}, /* auto power-off read (minutes) */
{{0x01, 0x0C}, handleUserFwVer}, /* user fw version */
{{0x01, 0x11}, handleAutoPowerOffSet}, /* auto power-off set (minutes) */
{{0x01, 0x0F}, handleFwCode}, /* fw code (MIDI/BLE-safe; replaces 01 FF) */
{{0x01, 0xFF}, handleFwCode}, /* fw code alias (raw UART only; 0xFF illegal in BLE-MIDI SysEx) */
//======== 3. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>д 0x03 ========
// {{0x03, 0x04}, handleRhythmStyle}, // <20><>ȡ/<2F><><EFBFBD>ý<EFBFBD><C3BD><EFBFBD><EFBFBD><EFBFBD>
{{0x03, 0x05}, handleStringTimbre}, // <20><>ȡ/<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɫ
{{0x03, 0x06}, handleBPM}, // <20><>ȡ/<2F><><EFBFBD><EFBFBD>BPM
{{0x03, 0x07}, handleTranspose}, // <20><>ȡ/<2F><><EFBFBD><EFBFBD><EFBFBD>Ƶ<EFBFBD>
/*======== 2. chord map 0x02 ========*/
{{0x02, 0x01}, handleReadRhythmMap}, /* read chord/pitch map */
{{0x02, 0x02}, handlePitchOffset}, /* pitch offset */
{{0x02, 0x03}, handleChordOffset}, /* chord offset */
{{0x02, 0x04}, handleResetChordMap}, /* write whole map (default reset) */
//======== 4. <20><><EFBFBD><EFBFBD>/LED<45><44><EFBFBD><EFBFBD> 0x04 ========
{{0x04, 0x00}, handleLED0}, // <20><>1<EFBFBD><31>LED
{{0x04, 0x01}, handleLED1}, // <20><>2<EFBFBD><32>LED
{{0x04, 0x02}, handleLED2}, // <20><>3<EFBFBD><33>LED
{{0x04, 0x03}, handleLED3}, // <20><>4<EFBFBD><34>LED
{{0x04, 0x04}, handleLED4}, // <20><>5<EFBFBD><35>LED
{{0x04, 0x05}, handleLED5}, // <20><>6<EFBFBD><36>LED
{{0x04, 0x06}, handleLED6}, // <20><>7<EFBFBD><37>LED
// {{0x04, 0x07}, handleStopPlay}, // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/*======== 3. guitar params 0x03 ========*/
{{0x03, 0x04}, handleRhythmStyle}, /* rhythm style r/w + user list */
{{0x03, 0x05}, handleStringTimbre}, /* string timbre r/w */
{{0x03, 0x06}, handleBPM}, /* BPM r/w */
{{0x03, 0x07}, handleTranspose}, /* transpose r/w */
// //======== 5. <20><>λ/<2F>ػ<EFBFBD> 0x05 ========
// {{0x05, 0x00}, handleDeviceReset}, // <20><EFBFBD><E8B1B8>λ
//
// //======== 6. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת 0x06 ========
{{0x06, 0x01}, handleIntro}, // ǰ<><C7B0>
{{0x06, 0x02}, handleInterlude}, // <20><><EFBFBD><EFBFBD>
{{0x06, 0x03}, handleOutro}, // β<><CEB2>
{{0x04, 0x07}, handleEnd}, // end
{{0x06, 0x05}, handleSectionA}, // A<><41>
{{0x06, 0x06}, handleSectionB}, // B<><42>
{{0x06, 0x07}, handleSectionC}, // C<><43>
{{0x06, 0x08}, handleSectionD}, // D<><44>
/*======== 4. play / LED 0x04 ========*/
{{0x04, 0x00}, handleLED0}, /* LED 1 */
{{0x04, 0x01}, handleLED1}, /* LED 2 */
{{0x04, 0x02}, handleLED2}, /* LED 3 */
{{0x04, 0x03}, handleLED3}, /* LED 4 */
{{0x04, 0x04}, handleLED4}, /* LED 5 */
{{0x04, 0x05}, handleLED5}, /* LED 6 */
{{0x04, 0x06}, handleLED6}, /* LED 7 */
{{0x04, 0x07}, handleEnd}, /* end / stop play */
/*======== 5. reset / power off 0x05 ========*/
{{0x05, 0x00}, handleDeviceReset}, /* device reset -> soft power off */
/*======== 6. section jump 0x06 ========*/
{{0x06, 0x01}, handleIntro}, /* intro */
{{0x06, 0x02}, handleInterlude}, /* interlude */
{{0x06, 0x03}, handleOutro}, /* outro */
{{0x06, 0x05}, handleSectionA}, /* section A */
{{0x06, 0x06}, handleSectionB}, /* section B */
{{0x06, 0x07}, handleSectionC}, /* section C */
{{0x06, 0x08}, handleSectionD}, /* section D */
};
// ָ<><D6B8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
@ -158,14 +163,16 @@ static const BLE_SysExCmdItem bleSysExCmdTable[] =
*/
void processBLESysEXData(uint8_t* data, uint8_t cnt)
{
//ResetAutoPowerCount();
// <20><><EFBFBD><EFBFBD>֡У<D6A1><D0A3>
// ֡У<D6A1><D0A3>
if (data[0] != FRAME_SYS_HEAD || data[cnt - 1] != FRAME_SYS_TAIL || cnt > UART4_PROCESS_BUFF_SIZE || data[1] != 0x60)
{
return;
}
/* App/BLE 活动视为用户操作,避免测协议时静置触发自动关机 */
ResetAutoPowerCount();
LOG_I("BLE", "sysex ok len=%u cmd=%02X %02X", (unsigned)cnt, data[2], data[3]);
// <20><>ȡ <20><>ָ<EFBFBD><D6B8> + <20><>ָ<EFBFBD><D6B8>
// ȡ<EFBFBD><EFBFBD>ָ<EFBFBD><EFBFBD> + <20><>ָ<EFBFBD><D6B8>
uint8_t cmd[2] = {data[2], data[3]};
// <20><><EFBFBD><EFBFBD>ָ<EFBFBD><D6B8><EFBFBD>ƥ<EFBFBD><C6A5>
@ -195,6 +202,7 @@ void UART4_Data_Process(volatile uint8_t* data)
last_tick = now;
memset(UART4_Process_Buff, 0, sizeof(UART4_Process_Buff));
UART4_RCV_cnt = 0;
UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
}
switch (UART4_RCV_Status)
@ -251,7 +259,6 @@ void UART4_Data_Process(volatile uint8_t* data)
}
else
{
// <20>Ƿ<EFBFBD><C7B7>ֽڣ<D6BD><DAA3><EFBFBD>λ
UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
}
break;
@ -346,8 +353,9 @@ static void handleTranspose(uint8_t *data)
{
case 0:
{
uint8_t ReturnTranspose[8] = {0xF0,0x60,0x03,0x07,0x00,0x00,0x00,0xF7};
ReturnTranspose[5] = mGuiData[GUI_TRANSPOSE].Current;
/* doc: F0 60 03 07 00 <val> F7 (7 bytes total) */
uint8_t ReturnTranspose[7] = {0xF0,0x60,0x03,0x07,0x00,0x00,0xF7};
ReturnTranspose[5] = (uint8_t)mGuiData[GUI_TRANSPOSE].Current;
USART4_SendData(ReturnTranspose,sizeof(ReturnTranspose));
}
break;
@ -472,25 +480,42 @@ static void handleLED6(uint8_t *data)
static void handlePitchOffset(uint8_t *data)
{
BL_Sendmsg(MSG_ID_PITCH_OFFSET, data[4]+1, data[5], 0);
uint8_t key = (uint8_t)(data[4] + 1u);
if (key < 1u || key > 21u)
return;
BL_Sendmsg(MSG_ID_PITCH_OFFSET, key, data[5], 0);
}
static void handleChordOffset(uint8_t *data)
{
BL_Sendmsg(MSG_ID_CHORD_OFFSET, data[4]+1, data[5], 0);
uint8_t key = (uint8_t)(data[4] + 1u);
if (key < 1u || key > 21u)
return;
BL_Sendmsg(MSG_ID_CHORD_OFFSET, key, data[5], 0);
}
extern CHORD_TYPE_INDEX chord_type_index_map[22];
static void handleReadRhythmMap(uint8_t *data)
{
uint8_t ReadChordMap[47] = {0xF0, 0x60, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7};
/* 静态缓冲:禁止在 TaskBTRecv 小栈上再开 47B */
static uint8_t ReadChordMap[47];
uint8_t chord_map = 0;
for(uint8_t i = 1;i < 22;i ++)
uint8_t i;
(void)data;
memset(ReadChordMap, 0, sizeof(ReadChordMap));
ReadChordMap[0] = 0xF0;
ReadChordMap[1] = 0x60;
ReadChordMap[2] = 0x02;
ReadChordMap[3] = 0x01;
ReadChordMap[46] = 0xF7;
for (i = 1; i < 22; i++)
{
ReadChordMap[3+i] = chord_type_index_map[i].PitchOffset;
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:
chord_map = 0;
@ -519,10 +544,13 @@ static void handleReadRhythmMap(uint8_t *data)
case 8:
chord_map = 11;
break;
default:
chord_map = 0;
break;
}
ReadChordMap[i] = chord_map;
}
USART4_SendData(ReadChordMap,sizeof(ReadChordMap));
USART4_SendData(ReadChordMap, sizeof(ReadChordMap));
}
static void handleIntro(uint8_t *data)
@ -546,7 +574,7 @@ static void handleEnd(uint8_t *data)
// TM1629D_AllLedOn(LED_COLOR_G);
// TM1629D_UpdateDisplay(0);
// osMutexRelease(Tm1629Mutex);
BL_Sendmsg(MSG_ID_ADCIN1KEY, 1, 0, 0);
BL_Sendmsg(MSG_ID_ADCIN1KEY, 2, 0, 0); /* End -> Postamble(1), distinct from Outro(key=1) */
}
static void handleSectionA(uint8_t *data)
@ -570,3 +598,270 @@ static void BL_Sendmsg(uint16_t Data1, uint16_t Data2, uint16_t Data3, uint16_t
{
BLTask_Sendmsg(Data1, Data2, Data3, Data4);
}
/* ======================================================================
* Appended protocol handlers (per Doc/.docx)
* ==================================================================== */
/*-------- 1. device info 0x01 --------*/
#define AT32_UID_BASE 0x1FFFF7E8UL /* 96-bit unique device ID */
/* 01 04 音源版本YY.M.D = 音色资源日期Dream 无版本查询,发版时手改)
* 26.9.8 2026-09-08Doc//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_0aedb4ASCIIBLE-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();
}

View File

@ -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 */

View File

@ -13,7 +13,7 @@ ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskUIThread_Stack[2048];
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskTouchThread_Stack[1536];
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskMainThread_Stack[512];
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskBTTHandlehread_Stack[512];
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskBTRecvThread_Stack[512];
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskBTRecvThread_Stack[768];
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskAutobandThread_Stack[512];
@ -115,6 +115,9 @@ void TaskBTHandleThread_entry(void* parameter)
case MSG_ID_LIGHT_LED : BL_Set_led(xEvent.ID,xEvent.HiByte); break;
case MSG_ID_PITCH_OFFSET : BT_Pitch_offset_map(xEvent.ID,xEvent.HiByte); break;
case MSG_ID_CHORD_OFFSET : BT_Chord_offset_map(xEvent.ID,xEvent.HiByte); break;
/* BLE section / intro-outro-end commands (06 xx, 04 07) */
case MSG_ID_ADCIN1KEY : ADC_IN1_KEY_Handle((uint8_t)xEvent.ID, true); break;
case MSG_ID_KEY_1617 : TM1617_Handle((uint8_t)xEvent.ID); break;
}
}
}
@ -125,11 +128,10 @@ void TaskBTRecvThread_entry(void* parameter)
uint8_t c;
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))
{
//解析函数
UART4_Data_Process(&c);
}
}
@ -238,6 +240,35 @@ void StartTask(void)
/* 扫描任务可能已由 StartScanTask 拉起;重复 startup 会被 RT-Thread 忽略 */
StartScanTask();
/* Soft-off(StopFullTask) 会 detach 下列线程;开机须能重建,否则 UI/消息永久失效 */
if ((TaskMainThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
{
rt_thread_init(&TaskMainThread,
"TaskMainThread",
TaskMainThread_entry,
RT_NULL,
&TaskMainThread_Stack,
sizeof(TaskMainThread_Stack),
5,
20);
}
if ((TaskMainThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_INIT)
rt_thread_startup(&TaskMainThread);
if ((TaskUIThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
{
rt_thread_init(&TaskUIThread,
"TaskUIThread",
TaskUIThread_entry,
RT_NULL,
&TaskUIThread_Stack,
sizeof(TaskUIThread_Stack),
5,
20);
}
if ((TaskUIThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_INIT)
rt_thread_startup(&TaskUIThread);
if ((TaskBTHandleThread.stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
{
rt_thread_init(&TaskBTHandleThread,
@ -334,18 +365,12 @@ void StopFullTask(void)
LOG_I("TASK", "stop ScanThread");
rt_thread_detach(&TaskScanThread);
}
if((TaskUIThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
LOG_I("TASK", "stop UIThread");
rt_thread_detach(&TaskUIThread);
}
/* 软关机 = 系统关闭:停触摸/扫描/BT/伴奏。
Main/UI Type-C Charging Image POWER_ON */
if((TaskTouchThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
LOG_I("TASK", "stop TouchThread");
rt_thread_detach(&TaskTouchThread);
}
if((TaskMainThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
LOG_I("TASK", "stop MainThread");
rt_thread_detach(&TaskMainThread);
}
if((TaskBTHandleThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
LOG_I("TASK", "stop BTHandleThread");
rt_thread_detach(&TaskBTHandleThread);
@ -354,6 +379,10 @@ void StopFullTask(void)
LOG_I("TASK", "stop BTRecvThread");
rt_thread_detach(&TaskBTRecvThread);
}
if((TaskAutobandThread.stat & RT_THREAD_STAT_MASK) != RT_THREAD_CLOSE) {
LOG_I("TASK", "stop AutobandThread");
rt_thread_detach(&TaskAutobandThread);
}
LOG_I("TASK", "StopFullTask done");
}

193
tools/_rtt_ble_diagnose.py Normal file
View File

@ -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())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1017 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 B

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 1.1 KiB

93
tools/flash_app_cspy.py Normal file
View File

@ -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()

View File

@ -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()

View File

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

View File

@ -61,8 +61,14 @@ def rgba_to_rgb565(im):
def rgba_to_rgb565_opaque(im):
"""Full-screen boot/charge: keep dark background pixels (no near-black punch-through)."""
im = im.convert("RGB")
"""Full-screen boot/charge/flash: keep dark bg; flatten RGBA onto black (not white).
Design exports like 烧录模式.png are mostly transparent with dark glyphs; a bare
convert('RGB') drops alpha and turns transparent into white wrong on device.
"""
rgba = im.convert("RGBA")
bg = Image.new("RGBA", rgba.size, (0, 0, 0, 255))
im = Image.alpha_composite(bg, rgba).convert("RGB")
px = im.load()
w, h = im.size
out = bytearray(w * h * 2)
@ -131,14 +137,14 @@ INTERNAL = [
("gImage_UI0902_Volume_20x15", "02_顶部图标/音量图标-10.png"),
("gImage_UI0902_Bluetooth_11x17", "02_顶部图标/蓝牙图标-10.png"),
# BatteryFill generated by make_battery_fill() — not from mockup PNG
("gImage_UI0902_TabSetting_Sel_36x28", "03_底部图标/底部图标-10.png"),
("gImage_UI0902_TabSetting_Not_35x28", "03_底部图标/底部图标-11.png"),
("gImage_UI0902_TabMixer_Sel_35x27", "03_底部图标/底部图标-12.png"),
("gImage_UI0902_TabMixer_Not_18x28", "03_底部图标/底部图标-13.png"),
("gImage_UI0902_TabMode_Sel_36x28", "03_底部图标/底部图标-14.png"),
("gImage_UI0902_TabMode_Not_35x29", "03_底部图标/底部图标-15.png"),
("gImage_UI0902_TabBack_Sel_35x27", "03_底部图标/底部图标-16.png"),
("gImage_UI0902_TabBack_Not_18x28", "03_底部图标/底部图标-17.png"),
("gImage_UI0902_TabSetting_Sel_40x32", "03_底部图标/底部图标-10.png"), # 万能白
("gImage_UI0902_TabSetting_Not_40x32", "03_底部图标/底部图标-11.png"), # 普通白
("gImage_UI0902_TabMixer_Sel_40x32", "03_底部图标/底部图标-12.png"), # 专业白
("gImage_UI0902_TabMixer_Not_22x32", "03_底部图标/底部图标-13.png"), # 设置白
("gImage_UI0902_TabMode_Sel_40x32", "03_底部图标/底部图标-14.png"), # 万能蓝
("gImage_UI0902_TabMode_Not_40x32", "03_底部图标/底部图标-15.png"), # 普通蓝
("gImage_UI0902_TabBack_Sel_40x32", "03_底部图标/底部图标-16.png"), # 专业蓝
("gImage_UI0902_TabBack_Not_22x32", "03_底部图标/底部图标-17.png"), # 设置蓝
]
# ---------------- external flash (big images / text strips) ----------------

387
tools/mic_pitch_analyze.py Normal file
View File

@ -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)

View File

@ -2,21 +2,23 @@
# -*- coding: utf-8 -*-
"""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)
0x0000CB70 Charg.bin (legacy pad; charge UI uses UI0902_CHARGE_SCREEN full-screen)
0x0001B8F0 1.bin 普通/专业 31 rhythms
0x0009D07D 2.bin 海阔天空
0x000A8607 3.bin 万能模式
0x0009D07D 2.bin 本地曲目变长尾部 0xFF 填到 BIN3<=41KB
0x000A71AC 3.bin 万能模式固定勿随 2.bin 长度漂移
Outputs:
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
Doc/音色文件/0903/FLASH_MAP.txt
"""
from __future__ import annotations
import os
import struct
import subprocess
import sys
from pathlib import Path
@ -25,11 +27,25 @@ ROOT = Path(__file__).resolve().parents[1] # firmware project root
REPO = ROOT.parent.parent # 一诺国际吉他
OUT_DIR = ROOT / "tools" / "out"
OUT_BIN = OUT_DIR / "extflash_tone_0903.bin"
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_MAP = REPO / "Doc" / "音色文件" / "0903" / "FLASH_MAP.txt"
UI0902_BIN = OUT_DIR / "ui0902_res.bin"
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:
for p in REPO.iterdir():
@ -38,29 +54,66 @@ def find_ziliao() -> Path:
raise FileNotFoundError("资料/logo.bin + Charg.bin not found under repo")
def dab_ok(blob: bytes, off: int, expect_cnt: int | None = None) -> None:
if off + 16 > len(blob):
raise SystemExit(f"DAB check @0x{off:X}: past end of pack ({len(blob)})")
magic = blob[off : off + 4]
cnt = struct.unpack_from("<I", blob, off + 12)[0]
if magic != DAB_MAGIC:
raise SystemExit(
f"DAB check @0x{off:X}: bad magic {magic.hex()} (want {DAB_MAGIC.hex()})"
)
if expect_cnt is not None and cnt != expect_cnt:
raise SystemExit(f"DAB check @0x{off:X}: cnt={cnt} want {expect_cnt}")
def require_region_equals(blob: bytes, off: int, src: bytes, label: str) -> None:
end = off + len(src)
if end > len(blob):
raise SystemExit(f"{label}: pack too short for region @0x{off:X}+{len(src)}")
if blob[off:end] != src:
raise SystemExit(f"{label}: bytes @0x{off:X} do not match source file")
def main() -> None:
ziliao = find_ziliao()
bin1_path = TONE_DIR / "1.bin"
bin2_path = TONE_DIR / "2.bin"
bin3_path = TONE_DIR / "3.bin"
parts = [
("LOGO", ziliao / "logo.bin", 0x00000000, "legacy pad; boot uses UI0902_BOOT_LOGO"),
("CHARGING", ziliao / "Charg.bin", 0x0000CB70, "legacy pad; UI uses UI0902_CHARGE_SCREEN"),
("BIN1_RHYTHM", TONE_DIR / "1.bin", 0x0001B8F0, "普通/专业 31 条节奏"),
("BIN2_SONG_HAITIAN", TONE_DIR / "2.bin", None, "本地曲目 海阔天空"),
("BIN3_UNIVERSAL", TONE_DIR / "3.bin", None, "万能模式"),
("LOGO", ziliao / "logo.bin", OFF_LOGO, "legacy pad; boot uses UI0902_BOOT_LOGO"),
("CHARGING", ziliao / "Charg.bin", OFF_CHARGING, "legacy pad; UI uses UI0902_CHARGE_SCREEN"),
("BIN1_RHYTHM", bin1_path, OFF_BIN1, "普通/专业 31 条节奏"),
("BIN2_SONG_HAITIAN", bin2_path, OFF_BIN2, "本地曲目"),
("BIN3_UNIVERSAL", bin3_path, OFF_BIN3, "万能模式(固定偏移)"),
]
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
rows = []
rows: list[tuple[str, int, int, str]] = []
for name, path, force_off, note in parts:
data = path.read_bytes()
if force_off is not None:
if cursor > force_off:
raise SystemExit(f"{name}: cursor 0x{cursor:X} past forced 0x{force_off:X}")
raise SystemExit(
f"{name}: cursor 0x{cursor:X} past forced 0x{force_off:X} "
f"(previous blob too large; enlarge next fixed gap or shrink prior file)"
)
if cursor < force_off:
pad = force_off - cursor
blobs.append(b"\xFF" * pad)
rows.append(("(pad)", cursor, pad, "gap fill 0xFF"))
cursor = force_off
rows.append((f"(pad)", force_off - pad, pad, "gap fill 0xFF"))
off = cursor
blobs.append(data)
cursor += len(data)
@ -69,9 +122,25 @@ def main() -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True)
packed = b"".join(blobs)
OUT_BIN.write_bytes(packed)
OUT_RES.write_bytes(packed) # 总音色 .bin 同步为 .res
# named lookup
by_name = {r[0]: r for r in rows if not r[0].startswith("(")}
bin1_data = bin1_path.read_bytes()
bin3_data = bin3_path.read_bytes()
# 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
#define __EXTFLASH_TONE_ADDR_H
@ -92,13 +161,13 @@ def main() -> None:
#define EXTFLASH_CHARGING_W 160
#define EXTFLASH_CHARGING_H 190
#define EXTFLASH_BIN1_RHYTHM_ADDR 0x{by_name['BIN1_RHYTHM'][1]:08X}UL
#define EXTFLASH_BIN1_RHYTHM_ADDR 0x{OFF_BIN1:08X}UL
#define EXTFLASH_BIN1_RHYTHM_SIZE {by_name['BIN1_RHYTHM'][2]}UL
#define EXTFLASH_BIN2_SONG_HAITIAN_ADDR 0x{by_name['BIN2_SONG_HAITIAN'][1]:08X}UL
#define EXTFLASH_BIN2_SONG_HAITIAN_ADDR 0x{OFF_BIN2:08X}UL
#define EXTFLASH_BIN2_SONG_HAITIAN_SIZE {by_name['BIN2_SONG_HAITIAN'][2]}UL
#define EXTFLASH_BIN3_UNIVERSAL_ADDR 0x{by_name['BIN3_UNIVERSAL'][1]:08X}UL
#define EXTFLASH_BIN3_UNIVERSAL_ADDR 0x{OFF_BIN3:08X}UL
#define EXTFLASH_BIN3_UNIVERSAL_SIZE {by_name['BIN3_UNIVERSAL'][2]}UL
/* Convenience aliases used by UI ADDRESS */
@ -121,8 +190,11 @@ def main() -> None:
map_lines = [
"K1 external Flash map — tone pack 0903",
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",
"",
"NOTE: BIN3 is FIXED at 0x000A71AC. 2.bin must be <= 41KB and <= slot 41263 bytes.",
"",
f"{'Name':<22} {'Offset':>10} {'Size':>10} Note",
"-" * 72,
]
@ -131,31 +203,52 @@ def main() -> None:
map_lines += [
"",
"Firmware ADDRESS mapping:",
" 普通/专业 -> FLASH_ADDR_MODE_NORMAL/EXPERT (1.bin)",
" 海阔天空 -> FLASH_ADDR_SONG_HAITIAN (2.bin)",
" 万能 -> FLASH_ADDR_MODE_UNIVERSAL (3.bin)",
" 普通/专业 -> FLASH_ADDR_MODE_NORMAL/EXPERT (1.bin @ 0x1B8F0)",
" 本地曲目 -> FLASH_ADDR_SONG_HAITIAN (2.bin @ 0x9D07D, max 41KB)",
" 万能 -> FLASH_ADDR_MODE_UNIVERSAL (3.bin @ 0xA71AC FIXED)",
" 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",
" Charging -> EXTFLASH_CHARGING_ADDR",
"",
"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)",
" 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")
print(f"Wrote {OUT_BIN} ({len(packed)} bytes)")
print(f"Wrote {OUT_RES} ({len(packed)} bytes)")
print(f"Wrote {OUT_HDR}")
print(f"Wrote {OUT_MAP}")
for name, off, size, note in rows:
print(f" 0x{off:08X} {size:8d} {name} {note}")
if cursor > 0x00100000:
if cursor > 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"
r = subprocess.run([sys.executable, str(gen)], check=False)
if r.returncode != 0:

View File

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
"""Build K1 release zip: MCU .bin + ExtFlash resources + SoundWalkerIAP + docs.
"""Build K1 release zip: Boot + MCU APP + ExtFlash resources + SoundWalkerIAP + docs.
External Flash has TWO regions that must both be present on a blank/erased chip:
0x00000000 tone pack (logo pad + Charg + 1/2/3.bin) -> *.tone.res / combined
0x00100000 UI0902 bitmaps (mode rows, boot logo, ...) -> *.ui0902.res / combined
Prefer flashing the combined *.extflash.res @ 0x0 for colleague upgrades.
Boot must also be updated so USB IAP wait screen can show UI0902_FLASH_MODE.
"""
from __future__ import annotations
@ -19,15 +20,26 @@ from pathlib import Path
REPO = Path(r"C:\Users\qjyu\Documents\SoundWalker\一诺国际吉他")
PROJ = REPO / "Code" / "YNGJ-GT1-M - AT32F403ARCT7"
EXE_BIN = PROJ / "project" / "IAR_V7.4" / "YNGJ-GT1-M" / "Exe" / "YNGJ-GT1-M.bin"
BOOT_BIN = (
PROJ
/ "AT32F403ARCT7_BOOT"
/ "project"
/ "IAR_V7.4"
/ "AT32F403ARCT7_BOOT"
/ "Exe"
/ "AT32F403ARCT7_BOOT.bin"
)
TONE_BIN = PROJ / "tools" / "out" / "extflash_tone_0903.bin"
UI0902_BIN = PROJ / "tools" / "out" / "ui0902_res.bin"
MAP_TXT = REPO / "Doc" / "音色文件" / "0903" / "FLASH_MAP.txt"
IAP_DIR = REPO / "升级" / "MCU主控升级"
OUT_ROOT = REPO / "tools" / "out"
FW_VER = "0.2.6"
FW_VER = "0.2.8"
TONE_RES_VER = "0903"
UI0902_RES_BASE = 0x00100000
BOOT_FLASH_ADDR = 0x08000000
APP_FLASH_ADDR = 0x08008000
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
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:
if not BOOT_BIN.is_file():
raise SystemExit(f"missing Boot bin: {BOOT_BIN}")
if not EXE_BIN.is_file():
raise SystemExit(f"missing MCU bin: {EXE_BIN}")
if not TONE_BIN.is_file():
@ -83,15 +124,20 @@ def main() -> None:
pkg.mkdir(parents=True)
tone_data = TONE_BIN.read_bytes()
verify_tone_layout(tone_data)
ui_data = UI0902_BIN.read_bytes()
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"
comb_name = f"extflash_ALL_tone0903_ui0902_{stamp}.res"
boot_dst = pkg / boot_name
mcu_dst = pkg / mcu_name
comb_dst = pkg / comb_name
shutil.copy2(BOOT_BIN, boot_dst)
shutil.copy2(EXE_BIN, mcu_dst)
comb_dst.write_bytes(combined)
shutil.copy2(iap, pkg / "SoundWalkerIAP.exe")
@ -99,6 +145,7 @@ def main() -> None:
if MAP_TXT.is_file():
shutil.copy2(MAP_TXT, pkg / "FLASH_MAP.txt")
shutil.copy2(boot_dst, OUT_ROOT / boot_name)
shutil.copy2(mcu_dst, OUT_ROOT / mcu_name)
shutil.copy2(comb_dst, OUT_ROOT / "extflash_ALL_tone0903_ui0902.res")
# Keep build intermediates under tools/out for local rebuilds; not shipped in package.
@ -115,26 +162,34 @@ def main() -> None:
f"Git subject {subject}",
f"FW version {FW_VER}",
"",
"==== 整机升级请刷这两项 ====",
f" 1) {mcu_name} -> MCU APP",
f" 2) {comb_name} -> 外部 Flash 从 0x0 起整包",
"==== 整机升级请刷这三项 ====",
f" 1) {boot_name} -> Bootloader @ 0x{BOOT_FLASH_ADDR:08X}",
f" 2) {mcu_name} -> MCU APP @ 0x{APP_FLASH_ADDR:08X}",
f" 3) {comb_name} -> 外部 Flash 从 0x0 起整包",
" (= 音色区 + 填充 + UI0902 图片区)",
"",
"建议步骤:",
" - 先刷 Boot否则 USB 烧录等待画面仍是旧红字「升级模式」)",
" - 再刷 APP",
" - 先整片擦除外部 Flash再刷上述 ALL .res @ 0x00000000",
" - 勿只刷音色区否则模式选择页会花屏UI 图在 0x100000",
"",
"外部 Flash 分区:",
" 0x00000000 音色/充电图 (toneRes)",
" logo pad + Charg@0xCB70 + 1.bin@0x1B8F0 + 2.bin(HKTK/2s) + 3.bin",
" logo pad + Charg@0xCB70 + 1.bin@0x1B8F0 + 2.bin@0x9D07D + 3.bin@0xA71AC(FIXED)",
" 0x00100000 UI0902 图片 (模式选择、开机 Logo、充电、烧录模式、设置/调音台等)",
" 0x001D2000 UI0902_FLASH_MODE 烧录模式全屏图Boot 读取)",
"",
f" {boot_name} size={boot_dst.stat().st_size} @ 0x{BOOT_FLASH_ADDR:08X}",
f" sha256={sha256(boot_dst)}",
f" {mcu_name} size={mcu_dst.stat().st_size} @ 0x{APP_FLASH_ADDR:08X}",
f" sha256={sha256(mcu_dst)}",
f" {comb_name} size={comb_dst.stat().st_size} @ 0x00000000",
f" sha256={sha256(comb_dst)}",
"",
f"MCU: {mcu_name} size={mcu_dst.stat().st_size} sha256={sha256(mcu_dst)}",
"",
"说明:",
" - Boot 进入 USB IAP 时显示 UI0902_FLASH_MODE资源未烧录时白字黑底兜底。",
" - 烧录模式图来源K1标准界面图 0904/烧录模式.png。",
" - 开机全屏 Logo、关机充电全屏画面在 UI0902不在音色包里的 logo.bin/Charg.bin 占位。",
" - AutoBand 0x9EB5F 本版未重排。",
"",

View File

@ -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()

276
tools/rtt_bin13_reg_test.py Normal file
View File

@ -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())

137
tools/rtt_mic_reg_test.py Normal file
View File

@ -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())

589
tools/rtt_pitch_reg_test.py Normal file
View File

@ -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()

View File

@ -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.patch01 03/0C
date YY.M.D 日期01 04 音源 / 01 05 UI并展开为 20YY-MM-DD
"""
if len(payload) != 4:
return f"ver_raw={payload!r}"
a, b, c, _d = payload
if kind == "date":
return f"ver={a}.{b}.{c} date=20{a:02d}-{b:02d}-{c:02d}"
return f"ver={a}.{b}.{c}"
def run_tests(sim, allow_poweroff=False):
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 0FBLE-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()

495
tools/test_protocol_ble.py Normal file
View File

@ -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 ASCII96-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="连接前 unpairWindows 默认开;--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()

253
tools/tone_artist_pack.py Normal file
View File

@ -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 必须 ≤41KB3.bin 地址固定,不可随 2.bin 变长漂移",
" - 若增删本地曲目或改曲名,需找开发同步固件后再测",
"",
]
)
path.write_text(text, encoding="utf-8", newline="\n")
def main() -> int:
ap = argparse.ArgumentParser(description="音师自助打包 1/2/3.bin → ALL.resUSB 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)} bytesALL.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)

View File

@ -0,0 +1,26 @@
音师投放与自助更新说明
========================
一、投放目录(放新的 1/2/3.bin
一诺国际吉他\Doc\音色文件\音师投放\
1.bin 普通/专业节奏
2.bin 本地曲目(必须 ≤41KB
3.bin 万能模式
二、一键打包(合并 ALL.resUSB 烧录,不用 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向开发索取一次基线即可

View File

@ -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