Compare commits
30 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
439ccf439d | |
|
|
2d62e1e41a | |
|
|
4aad989263 | |
|
|
295a2a6fa3 | |
|
|
b65b762d92 | |
|
|
30c4ea9c0d | |
|
|
05ccf28a4b | |
|
|
59c11f3f93 | |
|
|
24ffe87527 | |
|
|
5bb403cd11 | |
|
|
a818065c2e | |
|
|
c1b32c0351 | |
|
|
bb6d9ae7f9 | |
|
|
99f7ed3e5a | |
|
|
cd21e4e45e | |
|
|
92e5e32de3 | |
|
|
015417dc3d | |
|
|
6f2f7b6b75 | |
|
|
ba110565f1 | |
|
|
ad0d990c55 | |
|
|
f1f9520637 | |
|
|
57448b3da8 | |
|
|
0aedb45e6e | |
|
|
7b08f5e098 | |
|
|
f762348883 | |
|
|
8febbd7742 | |
|
|
bd22246942 | |
|
|
725ac495ba | |
|
|
01bc2a592c | |
|
|
a6259b5fca |
|
|
@ -303,6 +303,14 @@ void app_adc_sync_volume(uint16_t val)
|
|||
app_adc_volume_map_level(val);
|
||||
}
|
||||
|
||||
/* 取当前主音量(复用扫描任务维护的最近一次有效值)。
|
||||
* 模拟开关同一时刻只选中一路,离开扫描上下文直接重读 ADC 会串到
|
||||
* 其它通道,故统一走 vol_last;未初始化(0xFFFF)时按静音兜底 */
|
||||
uint8_t app_adc_get_volume(void)
|
||||
{
|
||||
return (vol_last > 127) ? 0 : (uint8_t)vol_last;
|
||||
}
|
||||
|
||||
static void app_adc_volume_process(void)
|
||||
{
|
||||
uint16_t val = ADC_ReadChannel(ADC_CHANNEL_1)>>5;
|
||||
|
|
|
|||
|
|
@ -27,4 +27,6 @@ void app_adc_in1_scan_part(uint8_t channel);
|
|||
bool app_adc_usb_is_plugged(void);
|
||||
/* 开机/主动采样后同步 vol_last 与 level,避免扫描任务误触发或跳变 */
|
||||
void app_adc_sync_volume(uint16_t val);
|
||||
/* 取当前主音量(0~127),供 MasterVolume_Reapply 等重发场景使用 */
|
||||
uint8_t app_adc_get_volume(void);
|
||||
#endif
|
||||
|
|
@ -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],
|
||||
|
|
@ -365,6 +399,20 @@ uint8_t app_log_try_command(const char *cmd)
|
|||
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;
|
||||
}
|
||||
/* 模拟专业+本地曲目拨片(无和弦板) */
|
||||
mGuiData[GUI_TAB_INDEX].Current = 1;
|
||||
mGuiData[GUI_AUTOBAND_SW].Current = 1;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
static uint8_t key_last = KEY_NONE;
|
||||
|
||||
|
||||
/* LED 期望颜色缓存:索引 = LedNum_TypeDef(0~7),COLOR_OTHER 表示灭 */
|
||||
/* LED ??????????¹×???? = LedNum_TypeDef??0~7????COLOR_OTHER ????? */
|
||||
//static LedColor_TypeDef led_cache[8] = {
|
||||
// COLOR_OTHER, COLOR_OTHER, COLOR_OTHER, COLOR_OTHER,
|
||||
// COLOR_OTHER, COLOR_OTHER, COLOR_OTHER, COLOR_OTHER
|
||||
|
|
@ -31,9 +31,14 @@ uint8_t app_tm1629_Scan_Key(void)
|
|||
if(key != key_last)
|
||||
{
|
||||
key_last = key;
|
||||
LOG_I("KEY", "tm1629 key=%u", (unsigned)key);
|
||||
if(key >= 1 && key <= 21)
|
||||
LOG_I("KEY", "tm1629 key=%u pitch=%u type=%u", (unsigned)key,
|
||||
(unsigned)chord_type_index_map[key].PitchOffset,
|
||||
(unsigned)chord_type_index_map[key].type);
|
||||
else
|
||||
LOG_I("KEY", "tm1629 key=%u", (unsigned)key);
|
||||
TM1629_Handle(key);
|
||||
return key;
|
||||
return key;
|
||||
}
|
||||
return KEY_NONE;
|
||||
}
|
||||
|
|
|
|||
112
Global/Global.c
|
|
@ -89,11 +89,11 @@ const Touch_AreaTypeDef Touch_Areas[] =
|
|||
{ 126, 172, UI0902_SECTION_Y, UI0902_SECTION_Y + UI0902_SECTION_BTN_H - 1, 0, 2, GUI_PLAY_SECTION, NULL },
|
||||
{ 183, 229, UI0902_SECTION_Y, UI0902_SECTION_Y + UI0902_SECTION_BTN_H - 1, 0, 3, GUI_PLAY_SECTION, NULL },
|
||||
|
||||
/* 底栏四键:万能 | 普通 | 专业 | 设置(触区中线对齐 cx 40/100/160/210) */
|
||||
{ 0, 69, 280, 319, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
|
||||
{ 70, 129, 280, 319, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
|
||||
{ 130, 184, 280, 319, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
|
||||
{ 185, 239, 280, 319, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
|
||||
/* 底栏四键:万能 | 普通 | 专业 | 设置(60×55,y265–319) */
|
||||
{ UI0902_NAV_X0(0), UI0902_NAV_X1(0), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_UNIVERSAL, GUI_NAV_BAR, NULL },
|
||||
{ UI0902_NAV_X0(1), UI0902_NAV_X1(1), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_NORMAL, GUI_NAV_BAR, NULL },
|
||||
{ UI0902_NAV_X0(2), UI0902_NAV_X1(2), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_EXPERT, GUI_NAV_BAR, NULL },
|
||||
{ UI0902_NAV_X0(3), UI0902_NAV_X1(3), UI0902_NAV_Y0, UI0902_NAV_Y1, 0, UI0902_NAV_SETTING, GUI_NAV_BAR, NULL },
|
||||
};
|
||||
|
||||
/* 推子 0~10 共 11 档;档10 取 AW816 上限附近 */
|
||||
|
|
@ -101,7 +101,7 @@ int16_t MIC_VOL_MAP[11]= {-9000,-7000,-5000,-3000,-1000,1000,3000,5000,7000,9000
|
|||
|
||||
uint8_t TOUCH_AREA_NUM = (sizeof(Touch_Areas)/sizeof(Touch_AreaTypeDef));
|
||||
|
||||
const uint8_t Version[3] = {0,2,6}; /* major.minor.patch — 0.2.6: local song 2.bin from Doc/音色文件/0908/2ss_2.bin */
|
||||
const uint8_t Version[3] = {0,2,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 Image(POWOFF_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);
|
||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||
}else
|
||||
{
|
||||
AutoBandTop1_SetPiecePlayMode(1,0,480);
|
||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||
}
|
||||
/* 设置页内沿用万能:与主路径一致,始终 loop */
|
||||
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_StartWithNotes(midi_note_buff, note_cnt);
|
||||
AutoBandTop1_SetLoopMode(0,1);
|
||||
}else
|
||||
{
|
||||
AutoBandTop1_SetPiecePlayMode(1,0,480);
|
||||
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
|
||||
}
|
||||
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,9 +982,12 @@ 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:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,64 @@ static uint8_t pStoreBuffer[PRESET_BUFFER_SIZE];
|
|||
//static uint8_t* pStoreBuffer;
|
||||
#define DEFAULT_TEMPO 100
|
||||
|
||||
/* ======== 路线A:#IV 降八度拦截(音师库支持八度前的临时方案) ========
|
||||
* AutoBand 库对输入音符只做音级识别,输出音区由其内部 octaveBase 决定,
|
||||
* 固件侧在 GetChordNotesByType 里的低八度设计不可听。故在引擎输出回调处
|
||||
* 按当前和弦状态对旋律通道做条件性八度偏移;音师新库到位后整体移除。 */
|
||||
#define CHORD_OCTAVE_INTERCEPT_EN 1
|
||||
#define DRUM_CHANNEL 9 /* 鼓通道不移调 */
|
||||
|
||||
/* 当前生效的八度偏移(0 或 -12)。RAM 余量极小,不用按音符表:
|
||||
* 状态翻转瞬间先向旋律通道补 CC123(All Notes Off) 清掉旧音区的音,
|
||||
* 之后 NoteOff 统一按当前偏移还原即可,不会挂音。 */
|
||||
static int8_t s_chord_octave_shift = 0;
|
||||
|
||||
static int8_t App_Auto_TargetChordOctaveShift(void)
|
||||
{
|
||||
#if CHORD_OCTAVE_INTERCEPT_EN
|
||||
/* 与 GetChordNotesByType 的根音八度设计对齐(整段伴奏压一个八度):
|
||||
* V/VI/VII(键 13~21)根音本就在低八度(43/45/47);
|
||||
* #IV(键 10~12 + Pitch 升)整组低八度,为最低音。
|
||||
* 引擎只认音级不认八度,故在输出侧统一补 -12。 */
|
||||
if (KEY_ID_1629 >= 13 && KEY_ID_1629 <= 21)
|
||||
return -12;
|
||||
if (KEY_ID_1629 >= 10 && KEY_ID_1629 <= 12 &&
|
||||
chord_type_index_map[KEY_ID_1629].PitchOffset == 2)
|
||||
return -12;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void App_Auto_QueueAllNotesOff(void)
|
||||
{
|
||||
MidiFifoItem_t m;
|
||||
for (int ch = 0; ch < 16; ch++)
|
||||
{
|
||||
if (ch == DRUM_CHANNEL) continue;
|
||||
m.msg[0] = 0xB0 | ch;
|
||||
m.msg[1] = 123; /* CC123 All Notes Off */
|
||||
m.msg[2] = 0;
|
||||
m.msglen = 3;
|
||||
mymidififo_InQueue(&m_MidiSendFifo,&m);
|
||||
}
|
||||
}
|
||||
|
||||
/* NoteOn 路径调用:偏移状态翻转时先清旧音,再返回新偏移 */
|
||||
static int8_t App_Auto_ShiftForNoteOn(void)
|
||||
{
|
||||
int8_t target = App_Auto_TargetChordOctaveShift();
|
||||
if (target != s_chord_octave_shift)
|
||||
{
|
||||
LOG_I("OCT", "shift %d->%d chord=%u pitch=%u",
|
||||
(int)s_chord_octave_shift, (int)target,
|
||||
(unsigned)KEY_ID_1629,
|
||||
(unsigned)chord_type_index_map[KEY_ID_1629].PitchOffset);
|
||||
App_Auto_QueueAllNotesOff();
|
||||
s_chord_octave_shift = target;
|
||||
}
|
||||
return s_chord_octave_shift;
|
||||
}
|
||||
|
||||
static void Func_CallBack_ProgramChange(int channel, int program)
|
||||
{
|
||||
MidiFifoItem_t midimsg;
|
||||
|
|
@ -32,8 +90,15 @@ 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;
|
||||
int out_key = key;
|
||||
if (channel != DRUM_CHANNEL && key >= 0 && key < 128)
|
||||
{
|
||||
/* 按当前偏移还原;偏移翻转瞬间的旧音已由 CC123 清掉 */
|
||||
out_key = key + s_chord_octave_shift;
|
||||
if (out_key < 0 || out_key > 127) out_key = key;
|
||||
}
|
||||
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 +108,17 @@ 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 = key;
|
||||
if (channel != DRUM_CHANNEL && key >= 0 && key < 128)
|
||||
{
|
||||
int8_t shift = (vel == 0) ? s_chord_octave_shift /* vel=0 视同 NoteOff */
|
||||
: App_Auto_ShiftForNoteOn();
|
||||
out_key = key + shift;
|
||||
if (out_key < 0 || out_key > 127) out_key = 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 +208,7 @@ static int Func_CallBack_ReadFlash(int address,int length,uint8_t* pOutput)
|
|||
//
|
||||
void App_Auto_Init(void)
|
||||
{
|
||||
LOG_I("OCT", "intercept build 20260910 en=%d", (int)CHORD_OCTAVE_INTERCEPT_EN);
|
||||
AutoBandTop1_SetPresetStoreBuffer(pStoreBuffer,PRESET_BUFFER_SIZE);
|
||||
AutoBandTop1_Init();
|
||||
AutoBandTop1_RegisterCallBack_NoteOn(Func_CallBack_NoteOn);
|
||||
|
|
@ -148,8 +222,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
|
||||
{
|
||||
|
|
|
|||
19
UI/UI_Idle.c
|
|
@ -43,12 +43,6 @@ static void UI_ModeSelect_DrawButtons(void)
|
|||
LCD_WR_PIC_FROM_FLASH(UI0902_MODE_ROW_X, y,
|
||||
UI0902_MODE_ROW_W, UI0902_MODE_ROW_H,
|
||||
s_mode_row_addr[i]);
|
||||
/* 万能模式素材顶部有残留近白横线(相对行内 y=7/8),盖成行底色 */
|
||||
if (i == 0) {
|
||||
LCD_FillByColor(UI0902_MODE_ROW_X, (uint16_t)(y + 7),
|
||||
(uint16_t)(UI0902_MODE_ROW_X + UI0902_MODE_ROW_W),
|
||||
(uint16_t)(y + 9), UI0902_ROW_BG);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -169,18 +163,19 @@ void IdleProcess(DisplayTaskMessage_Type msg)
|
|||
TM1629D_UpdateDisplay(0);
|
||||
StartTouchTask();
|
||||
StartScanTask();
|
||||
/* Dream 上电稍后可能忽略首包 SysEx:再发主音量 + 调音台(保持关机前设置) */
|
||||
/* Dream 上电稍后可能忽略首包 SysEx:再发主音量 + 调音台(保持关机前设置)。
|
||||
* 此时扫描任务已启动、模拟开关在轮询,不可现场重读 ADC(会串通道),
|
||||
* 统一用 MasterVolume_Reapply 取扫描任务维护的最近一次有效音量 */
|
||||
rt_thread_mdelay(400);
|
||||
vol = ADC_ReadChannel(ADC_CHANNEL_1)>>5;
|
||||
app_adc_sync_volume(vol);
|
||||
Send_volume(vol);
|
||||
MasterVolume_Reapply();
|
||||
LoadConfig();
|
||||
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, level, 1, WHITE, GRAY);
|
||||
break;
|
||||
|
||||
case MSG_ID_POWER_OFF:
|
||||
/* 关机:熄屏 + 关背光 */
|
||||
/* 无 Type-C:整机关机熄屏;有 Type-C 时随后 POWOFF_CHARG 会再亮充电图 */
|
||||
LCD_BLK_Clr();
|
||||
LCD_WR_REG(0x28); /* Display OFF */
|
||||
LCD_FillByColor(0, 0, 240, 320, BLACK);
|
||||
break;
|
||||
|
||||
|
|
@ -190,6 +185,8 @@ void IdleProcess(DisplayTaskMessage_Type msg)
|
|||
|
||||
case MSG_ID_TOUCH:
|
||||
{
|
||||
if (!powon)
|
||||
break; /* 软关机已停触摸;兜底勿进模式选择 */
|
||||
static uint32_t s_last_enter_ms;
|
||||
uint32_t now = rt_tick_get() * 1000U / RT_TICK_PER_SECOND;
|
||||
uint8_t hit = 0U;
|
||||
|
|
|
|||
|
|
@ -53,8 +53,33 @@ void DrawAllFaders(void)
|
|||
drv_nvm_save_to_flash();
|
||||
}
|
||||
|
||||
/* 0902-06:实心蓝色圆钮(勿用同心空心圆,会透出色条) */
|
||||
#define MIXER_KNOB_R 14
|
||||
/* 0902-06:圆钮定位半径 = 色条半宽;填充略大以盖住柱端抗锯齿白边(夹紧不画出柱外) */
|
||||
#define MIXER_KNOB_R 15
|
||||
#define MIXER_KNOB_FILL_R 16
|
||||
#define MIXER_TRACK_HALF_W 15
|
||||
|
||||
/* 与柱端半圆同形的实心圆钮:顶/底档位与色条圆角完美贴合,无虚线白边 */
|
||||
static void DrawMixerKnob(uint16_t cx, uint16_t cy)
|
||||
{
|
||||
int16_t dy;
|
||||
int32_t r2 = (int32_t)MIXER_KNOB_FILL_R * (int32_t)MIXER_KNOB_FILL_R;
|
||||
for (dy = -(int16_t)MIXER_KNOB_R; dy <= (int16_t)MIXER_KNOB_R; dy++)
|
||||
{
|
||||
int32_t xx = r2 - (int32_t)dy * (int32_t)dy;
|
||||
int16_t dx = 0;
|
||||
int16_t half;
|
||||
if (xx < 0)
|
||||
continue;
|
||||
while ((int32_t)(dx + 1) * (dx + 1) <= xx)
|
||||
dx++;
|
||||
half = dx;
|
||||
if (half > (int16_t)MIXER_TRACK_HALF_W)
|
||||
half = (int16_t)MIXER_TRACK_HALF_W;
|
||||
LCD_FillByColor((uint16_t)(cx - half), (uint16_t)(cy + dy),
|
||||
(uint16_t)(cx + half + 1), (uint16_t)(cy + dy + 1),
|
||||
COLOR_GRAD_MID);
|
||||
}
|
||||
}
|
||||
|
||||
static void DrawFaderSlider(Fader_t *f, uint16_t slider_y)
|
||||
{
|
||||
|
|
@ -62,9 +87,9 @@ static void DrawFaderSlider(Fader_t *f, uint16_t slider_y)
|
|||
uint16_t ly = f->y + f->h + 8;
|
||||
uint16_t cy = (uint16_t)(slider_y + MIXER_KNOB_R);
|
||||
char buf[8];
|
||||
LCD_FillCircle(cx, cy, MIXER_KNOB_R, COLOR_GRAD_MID);
|
||||
DrawMixerKnob(cx, cy);
|
||||
sprintf(buf, "%d", f->val);
|
||||
LCD_ShowString_AutoAlign((uint16_t)(cx - 8), (uint16_t)(cx + 8),
|
||||
LCD_ShowString_AutoAlign((uint16_t)(cx - 10), (uint16_t)(cx + 10),
|
||||
(uint16_t)(cy - 5),
|
||||
(uint8_t*)buf, LCD_ALIGN_CENTER, WHITE, COLOR_GRAD_MID, 0, 11);
|
||||
LCD_FillByColor(cx - 15, ly + 17, cx + 15, ly + 19, UI0902_BG_COLOR);
|
||||
|
|
@ -149,17 +174,14 @@ void UpdateFaderByTouch(int idx, uint16_t y)
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// ??????Y????????val? 0~10??
|
||||
// 滑块顶边 Y:val0/10 时圆心落在色条底/顶半圆中心,两端对称贴合
|
||||
// ============================================================
|
||||
static uint16_t CalcSliderY(Fader_t *f)
|
||||
{
|
||||
uint16_t slider_h = (uint16_t)(MIXER_KNOB_R * 2);
|
||||
uint16_t fill_h = (f->val * f->h) / 10;
|
||||
if (fill_h > f->h) fill_h = f->h;
|
||||
uint16_t slider_y = f->y + f->h - fill_h - (slider_h / 2);
|
||||
if (slider_y < f->y) slider_y = f->y;
|
||||
if (slider_y > f->y + f->h - slider_h) slider_y = f->y + f->h - slider_h;
|
||||
return slider_y;
|
||||
uint16_t top_cy = (uint16_t)(f->y + MIXER_KNOB_R);
|
||||
uint16_t bot_cy = (uint16_t)(f->y + f->h - 1 - MIXER_KNOB_R);
|
||||
uint16_t cy = (uint16_t)(bot_cy - ((uint32_t)(bot_cy - top_cy) * f->val) / 10);
|
||||
return (uint16_t)(cy - MIXER_KNOB_R);
|
||||
}
|
||||
|
||||
int CheckTouchFader(uint16_t x, uint16_t y)
|
||||
|
|
@ -222,7 +244,7 @@ void UI_Mixer_Process(DisplayTaskMessage_Type msg)
|
|||
break;
|
||||
|
||||
case MSG_ID_EC_VOL:
|
||||
Draw_Volume_Bar(UI0902_VOL_X, UI0902_VOL_Y, msg.HiByte, 1, WHITE, GRAY);
|
||||
/* 调音台子页无状态栏音量条(与返回键重叠);主音量已在 MainTask 下发 */
|
||||
ResetAutoPowerCount();
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -242,7 +242,9 @@ void Touch_Action(int8_t FLAG, uint8_t ID, uint8_t areaIndex,uint8_t value)
|
|||
mGuiData[ID].Current = value;
|
||||
AutoBandTop1_SetRunVar(mGuiData[ID].Current);
|
||||
}else
|
||||
{
|
||||
{
|
||||
/* 触摸起奏前补发主音量,保证实际音量与图标一致 */
|
||||
MasterVolume_Reapply();
|
||||
AutoBandTop1_FillinWithIndex(value);
|
||||
AutoBandTop1_Start();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ const Touch_AreaTypeDef Touch_SetingSelect_Areas[] = {
|
|||
{ UI0902_HUB_CARD_X, (uint16_t)(UI0902_HUB_CARD_X + UI0902_HUB_CARD_W - 1),
|
||||
UI0902_HUB_CARD_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;
|
||||
}
|
||||
|
|
|
|||
195
UI/UI_global.c
|
|
@ -54,6 +54,7 @@ void CallUI_Idle(void) {
|
|||
|
||||
void CallUI_SongMode(void) {
|
||||
app_touch_suppress(500);
|
||||
MasterVolume_Reapply(); /* 进模式补发主音量,防开机 SysEx 丢失后实际音量与图标不符 */
|
||||
UI_SongMode_Init();
|
||||
CurrUIProcress = UI_SongMode_Process;
|
||||
app_log_set_ui_page("Song");
|
||||
|
|
@ -62,8 +63,9 @@ void CallUI_SongMode(void) {
|
|||
|
||||
void CallUI_ExpertMode(void) {
|
||||
app_touch_suppress(500);
|
||||
MasterVolume_Reapply();
|
||||
UI_ExpertMode_Init();
|
||||
// CurrUIProcress = UI_ExpertMode_Process;
|
||||
// CurrUIProcress = UI_ExpertMode_Process;
|
||||
CurrUIProcress = UI_SongMode_Process;
|
||||
app_log_set_ui_page("Expert");
|
||||
LOG_I("UI", "page ExpertMode");
|
||||
|
|
@ -71,8 +73,9 @@ void CallUI_ExpertMode(void) {
|
|||
|
||||
void CallUI_FreeMode(void) {
|
||||
app_touch_suppress(500);
|
||||
MasterVolume_Reapply();
|
||||
UI_FreeMode_Init();
|
||||
// CurrUIProcress = UI_FreeMode_Process;
|
||||
// CurrUIProcress = UI_FreeMode_Process;
|
||||
CurrUIProcress = UI_SongMode_Process;
|
||||
app_log_set_ui_page("Free");
|
||||
LOG_I("UI", "page FreeMode");
|
||||
|
|
@ -168,10 +171,23 @@ void UI_ReloadTonePreset(void)
|
|||
if (mGuiData[GUI_TAB_INDEX].Current == 0)
|
||||
{
|
||||
int ret = AutoBandTop1_LoadPresetItemFromFlash(ParamGuiData[ALL_MODE_PARAM].Current);
|
||||
LOG_I("tone", "load universal idx=%d ret=%d name=%s addr=0x%08X map=BIN3@0x%08X",
|
||||
ParamGuiData[ALL_MODE_PARAM].Current, ret,
|
||||
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
|
||||
(unsigned)ADDRESS, (unsigned)EXTFLASH_BIN3_UNIVERSAL_ADDR);
|
||||
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=sizey,ASCII=UI0902_ASCII_W) */
|
||||
static uint16_t UI_MixedTextWidth(const uint8_t *str, uint8_t sizey)
|
||||
{
|
||||
uint16_t width = 0;
|
||||
uint8_t ascii_w = UI0902_ASCII_W(sizey);
|
||||
|
||||
if (str == NULL)
|
||||
return 0;
|
||||
while (*str != '\0') {
|
||||
if (*str > 0x80) {
|
||||
width = (uint16_t)(width + sizey);
|
||||
str += 2;
|
||||
} else {
|
||||
width = (uint16_t)(width + ascii_w);
|
||||
str += 1;
|
||||
}
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/* 曲目/音色共用:序号与名称间距(与「1.名称」拼接一致,保证两行视觉间距相同) */
|
||||
#define UI0902_INDEX_NAME_GAP 0
|
||||
|
||||
/* 在 [x1,x2] 内整体居中绘制「N.」+ 名称(无渐变,show_Param 用) */
|
||||
static void UI_DrawIndexedName_Mixed(uint16_t x1, uint16_t x2, uint16_t text_y,
|
||||
const char *num, const char *name,
|
||||
uint16_t fc, uint16_t bc, uint8_t sizey)
|
||||
{
|
||||
char line[48] = {0};
|
||||
snprintf(line, sizeof(line), "%s%s", num, name);
|
||||
LCD_ShowMixedString(x1, x2, text_y, (uint8_t *)line, LCD_ALIGN_CENTER,
|
||||
fc, bc, sizey, 1);
|
||||
}
|
||||
|
||||
/* 在 [x1,x2] 内整体居中绘制「N.」+ 中文名(渐变底,音色行用);间隙与曲目行一致 */
|
||||
static void UI_DrawIndexedName_Grad(uint16_t x1, uint16_t x2, uint16_t text_y,
|
||||
const char *num, const uint8_t *name,
|
||||
uint16_t fc, uint16_t c_top, uint16_t c_bot, uint8_t sizey)
|
||||
{
|
||||
uint16_t num_w = (uint16_t)(strlen(num) * UI0902_ASCII_W(sizey));
|
||||
uint16_t name_w = UI_MixedTextWidth(name, sizey);
|
||||
uint16_t total_w = (uint16_t)(num_w + UI0902_INDEX_NAME_GAP + name_w);
|
||||
uint16_t area_w = (x2 > x1) ? (uint16_t)(x2 - x1) : 0;
|
||||
uint16_t start_x = (total_w >= area_w) ? x1 : (uint16_t)(x1 + (area_w - total_w) / 2);
|
||||
uint16_t name_x = (uint16_t)(start_x + num_w + UI0902_INDEX_NAME_GAP);
|
||||
|
||||
LCD_ShowString_AutoAlign_Gradient(start_x, (uint16_t)(start_x + num_w), text_y,
|
||||
(const uint8_t *)num, LCD_ALIGN_LEFT, fc, c_top, c_bot, sizey);
|
||||
LCD_ShowChinese_AutoAlign_Gradient(name_x, x2, text_y, name, LCD_ALIGN_LEFT,
|
||||
fc, c_top, c_bot, sizey);
|
||||
}
|
||||
|
||||
void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
||||
{
|
||||
char Textstr_new[40] = {0};
|
||||
|
|
@ -453,9 +521,19 @@ void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
|||
const uint16_t text_y = row_y + (row_h - UI->Text_size) / 2 + 1;
|
||||
uint8_t idx = Group->Current + 1;
|
||||
uint16_t c_top, c_bot;
|
||||
uint16_t x1, x2;
|
||||
|
||||
UI_GetFocusGrad(GUI_MODE_PARAM, &c_top, &c_bot);
|
||||
|
||||
x1 = (uint16_t)(UI->x + 10);
|
||||
x2 = (uint16_t)(UI->x + UI->w - 10);
|
||||
|
||||
/* 修复曲目名尾部残留:旧名比新名长时,只填渐变胶囊(x+25 ~ x+w-40)
|
||||
盖不住文字区(x+10 ~ x+w-10)内的旧笔画,切到短名后尾部留残影。
|
||||
先按行底色清整个文字区,再补渐变与文字;整行清除会盖住两侧三角,
|
||||
文字绘制后统一补画。 */
|
||||
LCD_FillByColor(x1, row_y, x2, (uint16_t)(row_y + row_h), c_top);
|
||||
|
||||
LCD_FillRoundRectGradient(
|
||||
UI->x+25, row_y,
|
||||
UI->x + UI->w-40-(UI->x+25),
|
||||
|
|
@ -464,18 +542,21 @@ void show_Param(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
|||
c_top, c_bot
|
||||
);
|
||||
|
||||
// 拼接序号 1. 2. 3.
|
||||
/* 序号+名称整体居中(约等于标注「右移7px」的视觉效果) */
|
||||
sprintf(NumBuf, "%d.", idx);
|
||||
uint16_t num_w = (uint16_t)(strlen(NumBuf) * UI0902_ASCII_W(UI->Text_size));
|
||||
if (Group->Id == EXPRESS_MODE_PARAM && Group->Current < LOCAL_SONG_COUNT)
|
||||
strcpy(Textstr_new, LocalSongNameGbk[Group->Current]);
|
||||
else
|
||||
GET_ParamNum_Str(Group->Current, 0, Textstr_new, Group->Id);
|
||||
uint32_t string_start_x = LCD_ShowMixedString(UI->x+10, UI->x + UI->w, text_y, (uint8_t *)Textstr_new , LCD_ALIGN_CENTER, WHITE, BLACK, UI->Text_size, 1);
|
||||
//序号X = 中文起点 - 数字宽度 - 5,间<EFBC8C>?像素
|
||||
uint16_t num_x = string_start_x - num_w;
|
||||
// 绘制左侧数字序号
|
||||
LCD_ShowString(num_x, text_y, (const uint8_t *)NumBuf, UI->Text_fc, UI->Text_bc, UI->Text_size, 1);
|
||||
UI_DrawIndexedName_Mixed(x1, x2, text_y, NumBuf, Textstr_new,
|
||||
WHITE, BLACK, UI->Text_size);
|
||||
|
||||
/* 补画左右三角(几何与 label_ModeSelece_ui 一致) */
|
||||
{
|
||||
uint16_t row_cy = (uint16_t)(row_y + row_h / 2);
|
||||
UI_DrawTriLeft((uint16_t)(UI->x + 5), row_cy);
|
||||
UI_DrawTriRight((uint16_t)(UI->x + UI->w - 5 - UI0902_TRI_R_W), row_cy);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -803,12 +884,8 @@ void label_ModeSelece_ui(const LEBEL_UI *UI,GUI_SWITCH * Group)
|
|||
paramCurData = &ParamGuiData[SONG_MODE_PARAM];
|
||||
}
|
||||
|
||||
/* 左右三角由 show_Param 末尾统一补画(整行清除后必须重画) */
|
||||
show_Param(&UI_Label[GUI_MODE_PARAM], paramCurData);
|
||||
{
|
||||
uint16_t row_cy = UI->y + UI0902_TAB_H + 4 + (UI->h - UI0902_TAB_H - 4) / 2;
|
||||
UI_DrawTriLeft(UI->x + 5, row_cy);
|
||||
UI_DrawTriRight((uint16_t)(UI->x + UI->w - 5 - UI0902_TRI_R_W), row_cy);
|
||||
}
|
||||
}
|
||||
|
||||
void label_TimbreSelece_ui(const LEBEL_UI *UI, GUI_SWITCH * Group)
|
||||
|
|
@ -851,22 +928,17 @@ void label_TimbreSelece_ui(const LEBEL_UI *UI, GUI_SWITCH * Group)
|
|||
c_top, c_bot
|
||||
);
|
||||
sprintf(NumBuf, "%d.", idx);
|
||||
uint16_t num_w = (uint16_t)(strlen(NumBuf) * UI0902_ASCII_W(UI->Text_size));
|
||||
GET_Num_Str(mGuiData[UI->Id].Current, 0, (char*)Textstr, UI->Id);
|
||||
uint32_t chinese_start_x = LCD_ShowChinese_AutoAlign_Gradient(
|
||||
UI->x + num_w,
|
||||
UI->x + UI->w,
|
||||
text_y,
|
||||
(const uint8_t*)Textstr,
|
||||
UI->Text_Align,
|
||||
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);
|
||||
UI_DrawIndexedName_Grad(
|
||||
(uint16_t)(UI->x + 10),
|
||||
(uint16_t)(UI->x + UI->w - 10),
|
||||
text_y,
|
||||
NumBuf,
|
||||
Textstr,
|
||||
UI->Text_fc,
|
||||
c_top, c_bot,
|
||||
UI->Text_size
|
||||
);
|
||||
switch(Group->Id)
|
||||
{
|
||||
case GUI_TIMBRE_SELECT:
|
||||
|
|
@ -907,37 +979,32 @@ void UI_DrawSubPageHeader(const uint8_t *title_gbk)
|
|||
|
||||
void Draw_Bottom_Bar(uint8_t sel, uint8_t first_back)
|
||||
{
|
||||
/* 需求稿四键:万能 | 普通 | 专业 | 设置;first_back 时设置位仍高亮(返回用触控逻辑) */
|
||||
/* 四键合成图(图标+原稿文字);统一 32 高,文字底边对齐 */
|
||||
const uint8_t *icons[4];
|
||||
uint8_t ws[4], hs[4];
|
||||
/* 0904:四键中心约 40 / 100 / 160 / 210 */
|
||||
uint8_t ws[4];
|
||||
const uint16_t cx[4] = { 40, 100, 160, 210 };
|
||||
const uint8_t icon_h = 32;
|
||||
const uint16_t icon_y = 282;
|
||||
uint8_t i;
|
||||
|
||||
(void)first_back;
|
||||
/* 从段落钮底边之下清屏,勿切掉按钮底圆角(钮 y=231..264) */
|
||||
LCD_FillByColor(0, (uint16_t)(UI0902_SECTION_Y + UI0902_SECTION_BTN_H),
|
||||
240, 320, UI0902_BG_COLOR);
|
||||
/* mode.png:段落钮与底栏之间横线分隔(与顶栏同色同厚) */
|
||||
LCD_FillByColor(0, UI0902_SEP_LINE_Y, 240,
|
||||
(uint16_t)(UI0902_SEP_LINE_Y + UI0902_SEP_LINE_THICK),
|
||||
UI0902_SEP_LINE_COLOR);
|
||||
|
||||
if (sel == UI0902_NAV_UNIVERSAL) { icons[0] = gImage_Nav_Universal_Sel; ws[0] = 36; hs[0] = 28; }
|
||||
else { icons[0] = gImage_Nav_Universal_Not; ws[0] = 36; hs[0] = 28; }
|
||||
|
||||
if (sel == UI0902_NAV_NORMAL) { icons[1] = gImage_Nav_Normal_Sel; ws[1] = 35; hs[1] = 29; }
|
||||
else { icons[1] = gImage_Nav_Normal_Not; ws[1] = 35; hs[1] = 28; }
|
||||
|
||||
if (sel == UI0902_NAV_EXPERT) { icons[2] = gImage_Nav_Expert_Sel; ws[2] = 35; hs[2] = 27; }
|
||||
else { icons[2] = gImage_Nav_Expert_Not; ws[2] = 35; hs[2] = 27; }
|
||||
|
||||
if (sel == UI0902_NAV_SETTING) { icons[3] = gImage_Nav_Setting_Sel; ws[3] = 18; hs[3] = 28; }
|
||||
else { icons[3] = gImage_Nav_Setting_Not; ws[3] = 18; hs[3] = 28; }
|
||||
icons[0] = (sel == UI0902_NAV_UNIVERSAL) ? gImage_Nav_Universal_Sel : gImage_Nav_Universal_Not;
|
||||
icons[1] = (sel == UI0902_NAV_NORMAL) ? gImage_Nav_Normal_Sel : gImage_Nav_Normal_Not;
|
||||
icons[2] = (sel == UI0902_NAV_EXPERT) ? gImage_Nav_Expert_Sel : gImage_Nav_Expert_Not;
|
||||
icons[3] = (sel == UI0902_NAV_SETTING) ? gImage_Nav_Setting_Sel : gImage_Nav_Setting_Not;
|
||||
ws[0] = ws[1] = ws[2] = 40;
|
||||
ws[3] = 22;
|
||||
|
||||
for (i = 0; i < 4; i++)
|
||||
{
|
||||
LCD_WR_PIC_Trans(cx[i] - ws[i] / 2, 283, ws[i], hs[i], icons[i], BLACK);
|
||||
LCD_WR_PIC_Trans((uint16_t)(cx[i] - ws[i] / 2), icon_y,
|
||||
ws[i], icon_h, icons[i], BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -988,7 +1055,7 @@ void Draw_Tab_Section_Menu(const LEBEL_UI *UI, TAB_SECTION_INDEX select_tab)
|
|||
const uint8_t pitch = UI0902_SECTION_PITCH;
|
||||
const uint16_t start_x = UI0902_MARGIN_X; /* 0902-04:12/69/126/183 */
|
||||
const uint16_t start_y = UI->y;
|
||||
/* 0819:演奏段落数字与标题同为 13,按钮内垂直居中(视觉重心 +1) */
|
||||
/* 0819:演奏段落数字与标题同为 13,按钮内垂直居中(视觉重心 +1);稿面数字整体右移 2px */
|
||||
const uint16_t text_y = start_y + (tab_h - UI0902_FONT_BODY) / 2 + 1;
|
||||
const char *nums[4] = {"1", "2", "3", "4"};
|
||||
uint8_t i;
|
||||
|
|
@ -999,7 +1066,7 @@ void Draw_Tab_Section_Menu(const LEBEL_UI *UI, TAB_SECTION_INDEX select_tab)
|
|||
uint16_t bg = (select_tab == i) ? COLOR_GRAD_MID : UI0902_ROW_BG;
|
||||
LCD_FillRoundRect(x, start_y, tab_w, tab_h, tab_r, bg);
|
||||
LCD_ShowString_AutoAlign(
|
||||
x, (uint16_t)(x + tab_w - 1), text_y,
|
||||
(uint16_t)(x + 2), (uint16_t)(x + tab_w - 1 + 2), text_y,
|
||||
(const uint8_t*)nums[i],
|
||||
UI->Text_Align,
|
||||
WHITE,
|
||||
|
|
@ -1223,11 +1290,11 @@ void Draw_System_Item_List(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
|||
}
|
||||
break;
|
||||
case GUI_BLUETOOTH_SW:
|
||||
/* 素材:系统设置图标-11(白蓝牙符);Flash 槽名 RESTORE,尺寸 13x21 */
|
||||
/* 素材:系统设置图标-11(白蓝牙符)→ ICON_SET_BLUETOOTH 13x21 */
|
||||
LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12,
|
||||
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_RESTORE_H) / 2),
|
||||
UI0902_ICON_SYS_RESTORE_W, UI0902_ICON_SYS_RESTORE_H,
|
||||
UI0902_ICON_SYS_RESTORE_ADDR);
|
||||
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SET_BLUETOOTH_H) / 2),
|
||||
UI0902_ICON_SET_BLUETOOTH_W, UI0902_ICON_SET_BLUETOOTH_H,
|
||||
UI0902_ICON_SET_BLUETOOTH_ADDR);
|
||||
UI_DrawTriLeft(val_x1, chev_cy);
|
||||
if(Group->Current)
|
||||
LCD_ShowChinese_AutoAlign_Gradient(val_x1 + UI0902_TRI_L_W, val_x2 - UI0902_TRI_R_W, text_y,(const uint8_t*)"\xCA\xC7",LCD_ALIGN_CENTER, WHITE,c_top, c_bot,value_sz);
|
||||
|
|
@ -1264,13 +1331,17 @@ void Draw_System_Item_List(const LEBEL_UI *UI, GUI_SWITCH *Group)
|
|||
drv_nvm_save_to_flash();
|
||||
break;
|
||||
case GUI_RESTORE:
|
||||
/* 逆时针箭头:当前在 VERSION 槽(19x18);RESTORE 槽曾误为蓝牙符 */
|
||||
/* 素材:系统设置图标-13(逆时针恢复)→ ICON_SYS_RESTORE 19x18 */
|
||||
LCD_WR_PIC_FROM_FLASH_Trans(UI->x + 12,
|
||||
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_VERSION_H) / 2),
|
||||
UI0902_ICON_SYS_VERSION_W, UI0902_ICON_SYS_VERSION_H,
|
||||
UI0902_ICON_SYS_VERSION_ADDR);
|
||||
/* 右三角与蓝牙/自动关机同一右缘(val_x2) */
|
||||
UI_DrawTriRight((uint16_t)(val_x2 - UI0902_TRI_R_W), chev_cy);
|
||||
(uint16_t)(UI->y + (UI->h - UI0902_ICON_SYS_RESTORE_H) / 2),
|
||||
UI0902_ICON_SYS_RESTORE_W, UI0902_ICON_SYS_RESTORE_H,
|
||||
UI0902_ICON_SYS_RESTORE_ADDR);
|
||||
/* 0902-07:右侧为进入下级页的细 chevron(设置图标-03),勿用实心三角 */
|
||||
LCD_WR_PIC_FROM_FLASH_Trans(
|
||||
(uint16_t)(val_x2 - UI0902_ICON_CHEVRON_W),
|
||||
(uint16_t)(UI->y + (UI->h - UI0902_ICON_CHEVRON_H) / 2),
|
||||
UI0902_ICON_CHEVRON_W, UI0902_ICON_CHEVRON_H,
|
||||
UI0902_ICON_CHEVRON_ADDR);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,19 +173,25 @@ void CallUI_RestoreSelect(void);
|
|||
#define UI0902_NAV_EXPERT 2 /* 底栏:专业模式 */
|
||||
#define UI0902_NAV_SETTING 3 /* 底栏:设置 */
|
||||
#define UI0902_NAV_NONE 0xFF
|
||||
/* 底栏触区:段落钮底(y264)之下四等分全宽,各 60×55 */
|
||||
#define UI0902_NAV_Y0 265
|
||||
#define UI0902_NAV_Y1 319
|
||||
#define UI0902_NAV_W 60
|
||||
#define UI0902_NAV_X0(i) ((uint16_t)((i) * UI0902_NAV_W))
|
||||
#define UI0902_NAV_X1(i) ((uint16_t)((i) * UI0902_NAV_W + UI0902_NAV_W - 1))
|
||||
/* 兼容旧名(已废弃三键:设置|调音台|模式) */
|
||||
#define UI0902_NAV_MIXER UI0902_NAV_NORMAL
|
||||
#define UI0902_NAV_MODE UI0902_NAV_EXPERT
|
||||
|
||||
/* 底部图标实际内容(生成脚本曾把 万能/普通/专业/设置 误命名为 Setting/Mixer/Mode) */
|
||||
#define gImage_Nav_Universal_Not gImage_UI0902_TabSetting_Sel_36x28 /* 底部-10 白 */
|
||||
#define gImage_Nav_Universal_Sel gImage_UI0902_TabMode_Sel_36x28 /* 底部-14 蓝 */
|
||||
#define gImage_Nav_Normal_Not gImage_UI0902_TabSetting_Not_35x28 /* 底部-11 白 */
|
||||
#define gImage_Nav_Normal_Sel gImage_UI0902_TabMode_Not_35x29 /* 底部-15 蓝 */
|
||||
#define gImage_Nav_Expert_Not gImage_UI0902_TabMixer_Sel_35x27 /* 底部-12 白 */
|
||||
#define gImage_Nav_Expert_Sel gImage_UI0902_TabBack_Sel_35x27 /* 底部-16 蓝 */
|
||||
#define gImage_Nav_Setting_Not gImage_UI0902_TabMixer_Not_18x28 /* 底部-13 白 */
|
||||
#define gImage_Nav_Setting_Sel gImage_UI0902_TabBack_Not_18x28 /* 底部-17 蓝 */
|
||||
/* 底部图标:0909 加大图标 + 原稿文字条(合成图);生成脚本曾误命名 Setting/Mixer/Mode */
|
||||
#define gImage_Nav_Universal_Not gImage_UI0902_TabSetting_Sel_40x32 /* 底部-10 白 */
|
||||
#define gImage_Nav_Universal_Sel gImage_UI0902_TabMode_Sel_40x32 /* 底部-14 蓝 */
|
||||
#define gImage_Nav_Normal_Not gImage_UI0902_TabSetting_Not_40x32 /* 底部-11 白 */
|
||||
#define gImage_Nav_Normal_Sel gImage_UI0902_TabMode_Not_40x32 /* 底部-15 蓝 */
|
||||
#define gImage_Nav_Expert_Not gImage_UI0902_TabMixer_Sel_40x32 /* 底部-12 白 */
|
||||
#define gImage_Nav_Expert_Sel gImage_UI0902_TabBack_Sel_40x32 /* 底部-16 蓝 */
|
||||
#define gImage_Nav_Setting_Not gImage_UI0902_TabMixer_Not_22x32 /* 底部-13 白 */
|
||||
#define gImage_Nav_Setting_Sel gImage_UI0902_TabBack_Not_22x32 /* 底部-17 蓝 */
|
||||
/* 旧几何电池(Draw_Battery_Icon)最左起点;0902 图电池用 Draw_Status_Battery_Icon */
|
||||
#define UI_STATUS_BATTERY_X 150
|
||||
#define UI_STATUS_BATTERY_H 11
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@ extern const unsigned char gImage_UI0902_BatteryOutline_29x12[];
|
|||
extern const unsigned char gImage_UI0902_Volume_20x15[600];
|
||||
extern const unsigned char gImage_UI0902_Bluetooth_11x17[374];
|
||||
extern const unsigned char gImage_UI0902_BatteryFill_22x9[396];
|
||||
extern const unsigned char gImage_UI0902_TabSetting_Sel_36x28[2016];
|
||||
extern const unsigned char gImage_UI0902_TabSetting_Not_35x28[1960];
|
||||
extern const unsigned char gImage_UI0902_TabMixer_Sel_35x27[1890];
|
||||
extern const unsigned char gImage_UI0902_TabMixer_Not_18x28[1008];
|
||||
extern const unsigned char gImage_UI0902_TabSetting_Sel_40x32[2560];
|
||||
extern const unsigned char gImage_UI0902_TabSetting_Not_40x32[2560];
|
||||
extern const unsigned char gImage_UI0902_TabMixer_Sel_40x32[2560];
|
||||
extern const unsigned char gImage_UI0902_TabMixer_Not_22x32[1408];
|
||||
extern const unsigned char gImage_UI0902_TabMixer_Not_35x28[1960];
|
||||
extern const unsigned char gImage_UI0902_TabMode_Sel_36x28[2016];
|
||||
extern const unsigned char gImage_UI0902_TabMode_Not_35x29[2030];
|
||||
extern const unsigned char gImage_UI0902_TabBack_Sel_35x27[1890];
|
||||
extern const unsigned char gImage_UI0902_TabBack_Not_18x28[1008];
|
||||
extern const unsigned char gImage_UI0902_TabMode_Sel_40x32[2560];
|
||||
extern const unsigned char gImage_UI0902_TabMode_Not_40x32[2560];
|
||||
extern const unsigned char gImage_UI0902_TabBack_Sel_40x32[2560];
|
||||
extern const unsigned char gImage_UI0902_TabBack_Not_22x32[1408];
|
||||
|
||||
extern const unsigned char gImage_UI0902_BtnMinus_23x22[1012];
|
||||
extern const unsigned char gImage_UI0902_BtnPlus_23x22[1012];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// File: w25q128.c
|
||||
#include "w25q128.h"
|
||||
#include "wk_system.h"
|
||||
#include "rtthread.h"
|
||||
|
||||
/* W25Q128 挂在 SPI1 上(PA5=SCK, PA6=MISO, PA7=MOSI),CS 用 PA4 GPIO 控制 */
|
||||
#define W25Q128_SPI SPI1
|
||||
|
|
@ -82,6 +83,12 @@ static void W25Q128_WaitBusy(void)
|
|||
W25Q128_ReadWriteByte(W25X_ReadStatusReg1); // 发送读状态寄存器命令
|
||||
do {
|
||||
status = W25Q128_ReadWriteByte(0xFF); // 循环读取状态字
|
||||
if (status & 0x01) {
|
||||
SPI_CS_HIGH();
|
||||
rt_thread_mdelay(1);
|
||||
SPI_CS_LOW();
|
||||
W25Q128_ReadWriteByte(W25X_ReadStatusReg1);
|
||||
}
|
||||
} while (status & 0x01); // 等待BUSY位清零
|
||||
SPI_CS_HIGH();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,3 +37,22 @@ void SendMidiDataToDreamDSP(uint8_t* buff, uint16_t cnt)
|
|||
{
|
||||
USART2_SendData(buff,cnt);
|
||||
}
|
||||
|
||||
#define AT32_UID_BASE 0x1FFFF7E8UL
|
||||
|
||||
void BleBuildDeviceName(char *out)
|
||||
{
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
const uint32_t *uid = (const uint32_t *)AT32_UID_BASE;
|
||||
uint32_t tail = uid[2] & 0x00FFFFFFu;
|
||||
unsigned i;
|
||||
|
||||
out[0] = 'P';
|
||||
out[1] = 'H';
|
||||
out[2] = 'O';
|
||||
out[3] = 'N';
|
||||
out[4] = '_';
|
||||
for (i = 0; i < 6; i++)
|
||||
out[5 + i] = hex[(tail >> (4 * (5 - i))) & 0xFu];
|
||||
out[BLE_DEVICE_NAME_LEN] = '\0';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,4 +11,8 @@ void SendVolumeChangeMidiEvent(uint8_t channel, uint8_t CC);
|
|||
void SendMidiDataToDreamDSP(uint8_t* buff, uint16_t cnt);
|
||||
void DefaultTask_SendMsg(uint16_t Data1, uint16_t Data2, uint16_t Data3, uint16_t Data4);
|
||||
|
||||
/* Unique BLE/BT name from AT32 UID: "PHON_" + 6 hex (last 3 UID bytes). out >= 12. */
|
||||
#define BLE_DEVICE_NAME_LEN 11
|
||||
void BleBuildDeviceName(char *out);
|
||||
|
||||
#endif /* __MIDI_SEND_H__ */
|
||||
|
|
|
|||
|
|
@ -663,7 +663,7 @@
|
|||
<name>BUILDACTION</name>
|
||||
<archiveVersion>1</archiveVersion>
|
||||
<data>
|
||||
<prebuild></prebuild>
|
||||
<prebuild>python "$PROJ_DIR$\..\..\tools\gen_git_user_fw_ver.py"</prebuild>
|
||||
<postbuild></postbuild>
|
||||
</data>
|
||||
</settings>
|
||||
|
|
@ -1650,7 +1650,7 @@
|
|||
<name>BUILDACTION</name>
|
||||
<archiveVersion>1</archiveVersion>
|
||||
<data>
|
||||
<prebuild></prebuild>
|
||||
<prebuild>python "$PROJ_DIR$\..\..\tools\gen_git_user_fw_ver.py"</prebuild>
|
||||
<postbuild></postbuild>
|
||||
</data>
|
||||
</settings>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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-08(Doc/音色文件/0908) */
|
||||
static const uint8_t SOUND_VER[4] = {26, 9, 8, 0x01};
|
||||
/* 01 05 UI 资源/界面日期:YY.M.D(与 MCU 逻辑版本 Version[] 分开维护) */
|
||||
static const uint8_t UI_VER[4] = {26, 9, 9, 0x01};
|
||||
|
||||
static void handleDevDisconnect(uint8_t *data)
|
||||
{
|
||||
uint8_t resp[6] = {0xF0, 0x60, 0x01, 0x00, 0x01, 0xF7};
|
||||
(void)data;
|
||||
USART4_SendData(resp, sizeof(resp));
|
||||
}
|
||||
|
||||
static void handleDevConnect(uint8_t *data)
|
||||
{
|
||||
uint8_t resp[6] = {0xF0, 0x60, 0x01, 0x01, 0x01, 0xF7};
|
||||
(void)data;
|
||||
ResetAutoPowerCount();
|
||||
USART4_SendData(resp, sizeof(resp));
|
||||
}
|
||||
|
||||
static void handleDevName(uint8_t *data)
|
||||
{
|
||||
static char name[BLE_DEVICE_NAME_LEN + 1];
|
||||
static uint8_t resp[4 + BLE_DEVICE_NAME_LEN + 1]; /* head4 + name + F7 */
|
||||
(void)data;
|
||||
BleBuildDeviceName(name);
|
||||
resp[0] = 0xF0; resp[1] = 0x60; resp[2] = 0x01; resp[3] = 0x02;
|
||||
memcpy(&resp[4], name, BLE_DEVICE_NAME_LEN);
|
||||
resp[4 + BLE_DEVICE_NAME_LEN] = 0xF7;
|
||||
USART4_SendData(resp, (uint16_t)sizeof(resp));
|
||||
}
|
||||
|
||||
static void handleFwMainVer(uint8_t *data)
|
||||
{
|
||||
uint8_t resp[9] = {0xF0, 0x60, 0x01, 0x03, Version[0], Version[1], Version[2], 0x01, 0xF7};
|
||||
(void)data;
|
||||
USART4_SendData(resp, sizeof(resp));
|
||||
}
|
||||
|
||||
static void handleSoundVer(uint8_t *data)
|
||||
{
|
||||
uint8_t resp[9] = {0xF0, 0x60, 0x01, 0x04, SOUND_VER[0], SOUND_VER[1], SOUND_VER[2], SOUND_VER[3], 0xF7};
|
||||
(void)data;
|
||||
USART4_SendData(resp, sizeof(resp));
|
||||
}
|
||||
|
||||
static void handleUIVer(uint8_t *data)
|
||||
{
|
||||
uint8_t resp[9] = {0xF0, 0x60, 0x01, 0x05, UI_VER[0], UI_VER[1], UI_VER[2], UI_VER[3], 0xF7};
|
||||
(void)data;
|
||||
USART4_SendData(resp, sizeof(resp));
|
||||
}
|
||||
|
||||
static void handleOtherInfo(uint8_t *data)
|
||||
{
|
||||
/* 01 06: reserved — keep 8 zero bytes until product defines fields */
|
||||
uint8_t resp[13] = {0xF0, 0x60, 0x01, 0x06, 0, 0, 0, 0, 0, 0, 0, 0, 0xF7};
|
||||
(void)data;
|
||||
USART4_SendData(resp, sizeof(resp));
|
||||
}
|
||||
|
||||
static void handleDevCode(uint8_t *data)
|
||||
{
|
||||
/* 96-bit UID as 24 hex ASCII — SysEx data must stay 7-bit for BLE-MIDI */
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
uint8_t resp[29];
|
||||
const uint8_t *uid = (const uint8_t *)AT32_UID_BASE;
|
||||
uint8_t i;
|
||||
(void)data;
|
||||
resp[0] = 0xF0;
|
||||
resp[1] = 0x60;
|
||||
resp[2] = 0x01;
|
||||
resp[3] = 0x07;
|
||||
for (i = 0; i < 12u; i++)
|
||||
{
|
||||
resp[4u + 2u * i] = (uint8_t)hex[uid[i] >> 4];
|
||||
resp[4u + 2u * i + 1u] = (uint8_t)hex[uid[i] & 0x0Fu];
|
||||
}
|
||||
resp[28] = 0xF7;
|
||||
USART4_SendData(resp, sizeof(resp));
|
||||
}
|
||||
|
||||
static void handleAutoPowerOff(uint8_t *data)
|
||||
{
|
||||
uint8_t resp[6] = {0xF0, 0x60, 0x01, 0x0A, 0x00, 0xF7};
|
||||
(void)data;
|
||||
resp[4] = AutoCloseTime;
|
||||
USART4_SendData(resp, sizeof(resp));
|
||||
}
|
||||
|
||||
static void handleUserFwVer(uint8_t *data)
|
||||
{
|
||||
/* 用户固件版本 = "{branch}_{short6}"[+ '*'],如 develop_0aedb4(ASCII,BLE-MIDI 安全) */
|
||||
uint8_t resp[4u + GIT_BUILD_ID_LEN + 1u];
|
||||
(void)data;
|
||||
resp[0] = 0xF0;
|
||||
resp[1] = 0x60;
|
||||
resp[2] = 0x01;
|
||||
resp[3] = 0x0C;
|
||||
memcpy(&resp[4], GIT_BUILD_ID, GIT_BUILD_ID_LEN);
|
||||
resp[4u + GIT_BUILD_ID_LEN] = 0xF7;
|
||||
USART4_SendData(resp, (uint16_t)sizeof(resp));
|
||||
}
|
||||
|
||||
/* F0 60 01 11 <minutes> F7 : set auto power-off, 0 = disable */
|
||||
static void handleAutoPowerOffSet(uint8_t *data)
|
||||
{
|
||||
NvmParam_Type *nvm = drv_nvm_param_ptr();
|
||||
uint8_t resp[6] = {0xF0, 0x60, 0x01, 0x11, 0x00, 0xF7};
|
||||
AutoCloseTime = data[4];
|
||||
if (nvm->param.AutoCloseTime != AutoCloseTime)
|
||||
{
|
||||
nvm->param.AutoCloseTime = AutoCloseTime;
|
||||
drv_nvm_save_to_flash();
|
||||
}
|
||||
ResetAutoPowerCount();
|
||||
resp[4] = AutoCloseTime;
|
||||
USART4_SendData(resp, sizeof(resp));
|
||||
}
|
||||
|
||||
static void handleFwCode(uint8_t *data)
|
||||
{
|
||||
/* 固件编码: 请求 01 0F(推荐)或 01 FF(仅裸串口);应答统一 01 0F + ASCII,避免 BLE-MIDI 吃掉 0xFF */
|
||||
uint8_t resp[16];
|
||||
uint8_t len = (uint8_t)strlen(fwname);
|
||||
(void)data;
|
||||
if (len > 10u)
|
||||
len = 10u;
|
||||
resp[0] = 0xF0;
|
||||
resp[1] = 0x60;
|
||||
resp[2] = 0x01;
|
||||
resp[3] = 0x0F;
|
||||
memcpy(&resp[4], fwname, len);
|
||||
resp[4 + len] = 0xF7;
|
||||
USART4_SendData(resp, (uint16_t)(4u + len + 1u));
|
||||
}
|
||||
|
||||
/*-------- 2. chord map 0x02 --------*/
|
||||
/* F0 60 02 04 <21B pitch> <21B chord> F7 : write whole map; reply in 02 01 format */
|
||||
static void handleResetChordMap(uint8_t *data)
|
||||
{
|
||||
uint8_t key;
|
||||
for (key = 1; key <= 21; key++)
|
||||
{
|
||||
BT_Pitch_offset_map(key, data[3 + key]); /* data[4..24] pitch offsets */
|
||||
BT_Chord_offset_map(key, data[24 + key]); /* data[25..45] chord types */
|
||||
}
|
||||
handleReadRhythmMap(data);
|
||||
}
|
||||
|
||||
/*-------- 3. rhythm style 0x03 0x04 --------*/
|
||||
static void handleRhythmStyle(uint8_t *data)
|
||||
{
|
||||
switch (data[4])
|
||||
{
|
||||
case 0: /* read current style: F0 60 03 04 00 <src> <idHi> <idLo> F7 */
|
||||
{
|
||||
uint8_t resp[9] = {0xF0, 0x60, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0xF7};
|
||||
uint8_t src = (uint8_t)(mGuiData[GUI_AUTOBAND_SW].Current ? 1 : 0);
|
||||
uint16_t id = src ? ParamGuiData[EXPRESS_MODE_PARAM].Current
|
||||
: ParamGuiData[SONG_MODE_PARAM].Current;
|
||||
resp[5] = src;
|
||||
resp[6] = (uint8_t)(id / 128);
|
||||
resp[7] = (uint8_t)(id % 128);
|
||||
USART4_SendData(resp, sizeof(resp));
|
||||
}
|
||||
break;
|
||||
case 1: /* set style: F0 60 03 04 01 <src:0 sys/1 user> <idHi> <idLo> F7, reply BPM */
|
||||
{
|
||||
uint8_t src = (data[5] != 0) ? 1 : 0;
|
||||
uint16_t id = (uint16_t)data[6] * 128 + data[7];
|
||||
GUI_SWITCH *slot = src ? &ParamGuiData[EXPRESS_MODE_PARAM]
|
||||
: &ParamGuiData[SONG_MODE_PARAM];
|
||||
uint8_t resp[7] = {0xF0, 0x60, 0x03, 0x04, 0x01, 0x00, 0xF7};
|
||||
if (src)
|
||||
slot->Max = (LOCAL_SONG_COUNT > 0) ? (uint16_t)(LOCAL_SONG_COUNT - 1) : 0;
|
||||
if (id > slot->Max)
|
||||
id = slot->Max;
|
||||
mGuiData[GUI_AUTOBAND_SW].Current = src;
|
||||
slot->Current = id;
|
||||
StartFlag = 0;
|
||||
AutoBandTop1_Stop();
|
||||
UI_ReloadTonePreset();
|
||||
/* doc: single-byte bpm; clamp >127 (App should read exact bpm via 03 06) */
|
||||
resp[5] = (mGuiData[GUI_SPEED].Current > 0x7F)
|
||||
? 0x7F : (uint8_t)mGuiData[GUI_SPEED].Current;
|
||||
USART4_SendData(resp, sizeof(resp));
|
||||
|
||||
/* boot / song-entry special: F0 60 03 04 01 00 00 00 F7
|
||||
doc: also push current BPM as active report F0 51 03 <hi> <lo> F7 */
|
||||
if (data[5] == 0 && data[6] == 0 && data[7] == 0)
|
||||
{
|
||||
uint8_t bpmr[6] = {0xF0, 0x51, 0x03, 0x00, 0x00, 0xF7};
|
||||
bpmr[3] = (uint8_t)(mGuiData[GUI_SPEED].Current / 128);
|
||||
bpmr[4] = (uint8_t)(mGuiData[GUI_SPEED].Current % 128);
|
||||
USART4_SendData(bpmr, sizeof(bpmr));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 2: /* user style list page: F0 60 03 04 02 <from3B> <to3B> F7
|
||||
reply: F0 60 03 04 02 <cntHi> <cntLo> [<idHi> <idLo> <code 4B ascii> ...] F7 */
|
||||
{
|
||||
uint8_t resp[64];
|
||||
uint32_t from = 0, to = LOCAL_SONG_COUNT;
|
||||
uint16_t i, n = 0, len;
|
||||
/* from/to: 24-bit big-endian, optional (doc example: 00 00 00 07) */
|
||||
if (data[5] || data[6] || data[7])
|
||||
{
|
||||
from = ((uint32_t)data[5] << 16) | ((uint32_t)data[6] << 8) | data[7];
|
||||
to = ((uint32_t)data[8] << 16) | ((uint32_t)data[9] << 8) | data[10];
|
||||
}
|
||||
if (from > LOCAL_SONG_COUNT) from = LOCAL_SONG_COUNT;
|
||||
if (to > LOCAL_SONG_COUNT || to <= from) to = LOCAL_SONG_COUNT;
|
||||
resp[0] = 0xF0; resp[1] = 0x60; resp[2] = 0x03; resp[3] = 0x04; resp[4] = 0x02;
|
||||
len = 7; /* count filled after loop */
|
||||
for (i = (uint16_t)from; i < to; i++)
|
||||
{
|
||||
if (len + 6 + 1 > sizeof(resp)) break;
|
||||
resp[len++] = (uint8_t)(i / 128);
|
||||
resp[len++] = (uint8_t)(i % 128);
|
||||
memcpy(&resp[len], LocalSongCode[i], 4);
|
||||
len += 4;
|
||||
n++;
|
||||
}
|
||||
resp[5] = (uint8_t)(n / 128);
|
||||
resp[6] = (uint8_t)(n % 128);
|
||||
resp[len++] = 0xF7;
|
||||
USART4_SendData(resp, len);
|
||||
}
|
||||
break;
|
||||
case 3: /* user style total: reply F0 60 03 04 00 <total 3B BE> F7 (doc: 00 00 1E) */
|
||||
{
|
||||
uint8_t resp[9] = {0xF0, 0x60, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0xF7};
|
||||
resp[5] = (uint8_t)((LOCAL_SONG_COUNT >> 16) & 0xFF);
|
||||
resp[6] = (uint8_t)((LOCAL_SONG_COUNT >> 8) & 0xFF);
|
||||
resp[7] = (uint8_t)(LOCAL_SONG_COUNT & 0xFF);
|
||||
USART4_SendData(resp, sizeof(resp));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*-------- 5. reset / power off 0x05 --------*/
|
||||
/* F0 60 05 00 F7 : ack then soft power off (executed in main-loop context) */
|
||||
static void handleDeviceReset(uint8_t *data)
|
||||
{
|
||||
uint8_t resp[6] = {0xF0, 0x60, 0x05, 0x00, 0x01, 0xF7};
|
||||
(void)data;
|
||||
LOG_I("BLE", "05 00 ack then soft power off");
|
||||
USART4_SendData(resp, sizeof(resp));
|
||||
/* Soft-off cuts BT power immediately in PowerOff(); give UART4 + ATS2853
|
||||
time to push the ACK Notify before requesting power-off. */
|
||||
USART4_WaitTxIdle(50);
|
||||
rt_thread_mdelay(120);
|
||||
System_RequestPowerOff();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
/* Auto-generated by tools/gen_git_user_fw_ver.py — do not edit.
|
||||
* git HEAD (dirty): 30c4ea9c0de352263e5ca24547672b39c9d86e35
|
||||
* branch: develop → develop
|
||||
* 01 0C wire: develop_30c4ea*
|
||||
*/
|
||||
#ifndef GIT_USER_FW_VER_H
|
||||
#define GIT_USER_FW_VER_H
|
||||
|
||||
#define GIT_COMMIT_ID_FULL "30c4ea9c0de352263e5ca24547672b39c9d86e35"
|
||||
#define GIT_BRANCH_NAME "develop"
|
||||
#define GIT_COMMIT_SHORT6 "30c4ea"
|
||||
#define GIT_DIRTY (1)
|
||||
#define GIT_BUILD_ID "develop_30c4ea*"
|
||||
#define GIT_BUILD_ID_LEN 15u
|
||||
|
||||
#endif /* GIT_USER_FW_VER_H */
|
||||
|
|
@ -13,7 +13,7 @@ ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskUIThread_Stack[2048];
|
|||
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t TaskTouchThread_Stack[1536];
|
||||
ALIGN(RT_ALIGN_SIZE) static rt_uint8_t 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -124,12 +127,11 @@ 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,8 +379,12 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void OutTime_Stop_timer_callback(void *parameter)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,193 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Flash is separate; this attaches RTT then writes BLE SysEx and prints MCU logs."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pylink
|
||||
from bleak import BleakClient, BleakScanner
|
||||
|
||||
NAME = "Smart Guitar MIDI"
|
||||
MIDI = "7772e5db-3868-4112-a1a9-f2669d106bf3"
|
||||
UARTW = "e49a25e0-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||
UARTN = "e49a28e1-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||
EFF2 = "0000eff2-0000-1000-8000-00805f9b34fb"
|
||||
|
||||
|
||||
def open_jlink():
|
||||
j = pylink.JLink()
|
||||
j.open()
|
||||
try:
|
||||
j.exec_command("HideDeviceSelection = 1")
|
||||
except Exception:
|
||||
pass
|
||||
j.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
||||
last = None
|
||||
for dev in ("Cortex-M4", "AT32F403AC", "AT32F403A"):
|
||||
try:
|
||||
try:
|
||||
j.exec_command(f"Device = {dev}")
|
||||
except Exception:
|
||||
pass
|
||||
j.connect(dev)
|
||||
print("JLink OK", dev, flush=True)
|
||||
try:
|
||||
j.restart(halt=False)
|
||||
except Exception:
|
||||
try:
|
||||
j.go()
|
||||
except Exception:
|
||||
pass
|
||||
return j
|
||||
except Exception as e:
|
||||
last = e
|
||||
raise SystemExit(f"JLink fail: {last}")
|
||||
|
||||
|
||||
async def find_dev(timeout=45.0):
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
d = await BleakScanner.find_device_by_filter(
|
||||
lambda d, a: d.name and NAME.lower() in d.name.lower(), timeout=8
|
||||
)
|
||||
if d:
|
||||
return d
|
||||
print("wait BLE...", flush=True)
|
||||
return None
|
||||
|
||||
|
||||
async def ble_write(tag, write_uuid, payload, notify_uuids):
|
||||
d = await find_dev()
|
||||
if not d:
|
||||
print("NO BLE", flush=True)
|
||||
return
|
||||
print(f"BLE {d.address} write {tag} {payload.hex()}", flush=True)
|
||||
try:
|
||||
c = BleakClient(d.address, timeout=20)
|
||||
await c.connect()
|
||||
try:
|
||||
await c.unpair()
|
||||
except Exception:
|
||||
pass
|
||||
await c.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(1.0)
|
||||
d = await find_dev() or d
|
||||
notifs = []
|
||||
async with BleakClient(d, timeout=25) as c:
|
||||
def cb(_s, data):
|
||||
notifs.append(bytes(data))
|
||||
print("GATT NOTIFY", data.hex(), flush=True)
|
||||
|
||||
for u in notify_uuids:
|
||||
try:
|
||||
await c.start_notify(u, cb)
|
||||
except Exception as e:
|
||||
print("notify fail", u[:8], e, flush=True)
|
||||
await asyncio.sleep(0.2)
|
||||
try:
|
||||
await c.write_gatt_char(write_uuid, payload, response=False)
|
||||
print("GATT write ok, conn", c.is_connected, flush=True)
|
||||
except Exception as e:
|
||||
print("GATT write fail", e, flush=True)
|
||||
await asyncio.sleep(3.0)
|
||||
print("GATT notifs", len(notifs), "conn", c.is_connected, flush=True)
|
||||
|
||||
|
||||
async def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
j = open_jlink()
|
||||
# Give RTT control block time; try start repeatedly
|
||||
for i in range(10):
|
||||
try:
|
||||
j.rtt_start(block_address=0x20016D68)
|
||||
st = j.rtt_get_status()
|
||||
print("RTT status", st, flush=True)
|
||||
if getattr(st, "NumUpBuffers", 0):
|
||||
break
|
||||
except Exception as e:
|
||||
print("rtt_start", e, flush=True)
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
print("WARN: RTT upbuffers still 0 — will keep reading anyway", flush=True)
|
||||
|
||||
stop = False
|
||||
lines = []
|
||||
|
||||
def reader():
|
||||
while not stop:
|
||||
try:
|
||||
data = j.rtt_read(0, 4096)
|
||||
if data:
|
||||
s = bytes(data).decode("utf-8", "replace")
|
||||
lines.append(s)
|
||||
print("RTT>", s, end="" if s.endswith("\n") else "\n", flush=True)
|
||||
except Exception as e:
|
||||
print("rtt read err", e, flush=True)
|
||||
break
|
||||
time.sleep(0.03)
|
||||
|
||||
th = threading.Thread(target=reader, daemon=True)
|
||||
th.start()
|
||||
print("collect 4s boot/idle logs...", flush=True)
|
||||
await asyncio.sleep(4.0)
|
||||
if not lines:
|
||||
print("WARNING: no RTT yet — touch screen or wait; continuing BLE test", flush=True)
|
||||
|
||||
# 1) BLE-MIDI framed (stable path)
|
||||
await ble_write(
|
||||
"midi-framed",
|
||||
MIDI,
|
||||
bytes.fromhex("8080F0600101F7"),
|
||||
[MIDI, UARTN, EFF2],
|
||||
)
|
||||
await asyncio.sleep(2.0)
|
||||
|
||||
# E49A raw write often drops link / stops advertising — skip by default
|
||||
if "--with-uartw" in sys.argv:
|
||||
await ble_write(
|
||||
"uartw-raw",
|
||||
UARTW,
|
||||
bytes.fromhex("F0600101F7"),
|
||||
[UARTN, MIDI, EFF2],
|
||||
)
|
||||
await asyncio.sleep(2.0)
|
||||
stop = True
|
||||
time.sleep(0.4)
|
||||
j.close()
|
||||
|
||||
text = "".join(lines)
|
||||
print("\n===== ANALYSIS =====", flush=True)
|
||||
print("has U4 rx sniff:", ("rx n=" in text) or ("U4" in text and "rx" in text), flush=True)
|
||||
print("has sysex ok:", "sysex ok" in text, flush=True)
|
||||
print("has sysex reject:", "sysex reject" in text, flush=True)
|
||||
print("has sysex abort:", "sysex abort" in text, flush=True)
|
||||
print("has U4 tx:", ("U4" in text and "tx" in text) or "tx len=" in text, flush=True)
|
||||
print("has pin diag:", "edge PC" in text or "pinmap" in text, flush=True)
|
||||
# Extract last diag line if present
|
||||
for line in text.splitlines():
|
||||
if "edge PC10=" in line:
|
||||
print("diag:", line.strip(), flush=True)
|
||||
if "sysex ok" in text and "tx len=" in text:
|
||||
print("VERDICT: MCU got cmd and replied on UART4 → GATT notify path broken in BLE module", flush=True)
|
||||
elif "rx n=" in text and "sysex ok" not in text:
|
||||
print("VERDICT: UART4 got bytes but frame not accepted → protocol/framing", flush=True)
|
||||
elif "edge PC12=" in text and "isr_rx=0" in text:
|
||||
# SCH puts B_UART4_RX on PC12; FW UART4 RX is PC11
|
||||
print("VERDICT: activity on PC12 while UART4 ISR idle → driver pinmap vs SCH (MCU底层)", flush=True)
|
||||
elif "edge PC11=" in text and "isr_rx=0" in text:
|
||||
print("VERDICT: edges on PC11 but no UART ISR → baud/noise or not UART framing", flush=True)
|
||||
elif "isr_rx=0" in text and "edge PC10=0" in text and "edge PC11=0" in text and "edge PC12=0" in text:
|
||||
print("VERDICT: no GPIO edges + no UART ISR → ATS2853 did not drive UART (模组固件/桥接)", flush=True)
|
||||
elif "U4" not in text and "rx" not in text:
|
||||
print("VERDICT: no UART4 activity → BLE module did not forward GATT write to UART4 (or RTT dead)", flush=True)
|
||||
else:
|
||||
print("VERDICT: inconclusive — see RTT dump above", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1017 B |
|
Before Width: | Height: | Size: 902 B After Width: | Height: | Size: 845 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,93 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Flash APP via cspybat (AT32F403AC) then J-Link reset."""
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
BASE = Path(r"C:\Users\qjyu\Documents\SoundWalker\一诺国际吉他\Code\YNGJ-GT1-M - AT32F403ARCT7")
|
||||
STAGE = Path(r"C:\Temp\k1flash_boot")
|
||||
CSPY = Path(r"C:\Program Files (x86)\IAR Systems\Embedded Workbench 7.3\common\bin\cspybat.exe")
|
||||
JLINK = Path(r"C:\Program Files\SEGGER\JLink_V818\JLink.exe")
|
||||
|
||||
APP_OUT = BASE / "project" / "IAR_V7.4" / "YNGJ-GT1-M" / "Exe" / "YNGJ-GT1-M.out"
|
||||
GEN_TMPL = BASE / "project" / "IAR_V7.4" / "settings" / "YNGJ-GT1-M.YNGJ-GT1-M.general.xcl"
|
||||
DRV_TMPL = BASE / "project" / "IAR_V7.4" / "settings" / "YNGJ-GT1-M.YNGJ-GT1-M.driver.xcl"
|
||||
RESET_JLINK = BASE / "tools" / "reset_run.jlink"
|
||||
|
||||
|
||||
def kill_debuggers() -> None:
|
||||
for n in (
|
||||
"cspybat.exe", "CSpyBat.exe", "JLink.exe", "IarIdePm.exe",
|
||||
"JLinkGUIServer.exe", "JLinkRTTClient.exe", "JFlash.exe",
|
||||
):
|
||||
subprocess.run(["taskkill", "/F", "/IM", n], capture_output=True)
|
||||
time.sleep(1.0)
|
||||
|
||||
|
||||
def stage_xcl(out_file: Path):
|
||||
STAGE.mkdir(parents=True, exist_ok=True)
|
||||
staged_out = STAGE / out_file.name
|
||||
shutil.copy2(out_file, staged_out)
|
||||
|
||||
gen_lines = GEN_TMPL.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
new_gen = []
|
||||
for ln in gen_lines:
|
||||
if ".out" in ln and ("YNGJ" in ln or "BOOT" in ln or "AT32" in ln):
|
||||
new_gen.append(f'"{staged_out}" ')
|
||||
else:
|
||||
new_gen.append(ln)
|
||||
gen_path = STAGE / "general.xcl"
|
||||
gen_path.write_text("\n".join(new_gen) + "\n", encoding="utf-8", newline="\n")
|
||||
|
||||
drv_lines = DRV_TMPL.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
forced = [ln for ln in drv_lines if not ln.strip().startswith("--jlink_device")]
|
||||
forced.append("--jlink_device=AT32F403AC")
|
||||
drv_path = STAGE / "driver.xcl"
|
||||
drv_path.write_text("\n".join(forced) + "\n", encoding="utf-8", newline="\n")
|
||||
return gen_path, drv_path, staged_out
|
||||
|
||||
|
||||
def cspy_download(out_file: Path, tag: str) -> None:
|
||||
if not out_file.is_file():
|
||||
raise SystemExit(f"missing {out_file}")
|
||||
gen_path, drv_path, staged_out = stage_xcl(out_file)
|
||||
logp = STAGE / f"cspy_{tag}.log"
|
||||
cmd = [
|
||||
str(CSPY), "-f", str(gen_path),
|
||||
f"--debug_file={staged_out}",
|
||||
"--download_only", "--backend", "-f", str(drv_path),
|
||||
]
|
||||
print(f"cspybat download {tag}: {staged_out.name}", flush=True)
|
||||
with logp.open("w", encoding="utf-8", errors="replace") as log:
|
||||
r = subprocess.run(cmd, stdout=log, stderr=subprocess.STDOUT, timeout=240)
|
||||
text = logp.read_text(encoding="utf-8", errors="replace")
|
||||
print(text[-1500:], flush=True)
|
||||
if r.returncode != 0:
|
||||
raise SystemExit(f"cspybat {tag} failed rc={r.returncode}")
|
||||
|
||||
|
||||
def jlink_reset() -> None:
|
||||
print("J-Link reset/run...", flush=True)
|
||||
subprocess.run(
|
||||
[
|
||||
str(JLINK), "-Device", "AT32F403AC", "-If", "SWD",
|
||||
"-Speed", "4000", "-AutoConnect", "1",
|
||||
"-CommandFile", str(RESET_JLINK),
|
||||
],
|
||||
capture_output=True, timeout=45,
|
||||
)
|
||||
time.sleep(2.5)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
kill_debuggers()
|
||||
cspy_download(APP_OUT, "app")
|
||||
jlink_reset()
|
||||
print("FLASH OK", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Flash Doc/音色文件/0903/3.bin to W25Q128 @ EXTFLASH_BIN3_UNIVERSAL_ADDR via RTT.
|
||||
|
||||
Requires firmware that accepts RTT command: flash bin3 <size>
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import time
|
||||
|
||||
DEFAULT_DEVICES = ("Cortex-M4", "AT32F403AC", "AT32F403A")
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
REPO = os.path.dirname(os.path.dirname(ROOT))
|
||||
DEFAULT_BIN = os.path.join(REPO, "Doc", "音色文件", "0903", "3.bin")
|
||||
EXTFLASH_BIN3_UNIVERSAL_ADDR = 0x000A71AC
|
||||
DAB_MAGIC = bytes((0xAB, 0x44, 0x41, 0x42))
|
||||
|
||||
|
||||
def connect_jlink(device: str):
|
||||
import pylink
|
||||
|
||||
jlink = pylink.JLink()
|
||||
jlink.open()
|
||||
try:
|
||||
jlink.exec_command("HideDeviceSelection = 1")
|
||||
except Exception:
|
||||
pass
|
||||
jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
||||
|
||||
last_error = None
|
||||
for candidate in (device, *DEFAULT_DEVICES):
|
||||
try:
|
||||
try:
|
||||
jlink.exec_command(f"Device = {candidate}")
|
||||
except Exception:
|
||||
pass
|
||||
jlink.connect(candidate)
|
||||
print(f"Connected as {candidate}")
|
||||
return jlink
|
||||
except Exception as exc:
|
||||
last_error = exc
|
||||
jlink.close()
|
||||
raise SystemExit(f"Failed to connect J-Link: {last_error}")
|
||||
|
||||
|
||||
def find_rtt_control_block(jlink, ram_base: int = 0x20000000, ram_size: int = 0x18000):
|
||||
needle = b"SEGGER RTT"
|
||||
chunk = 0x1000
|
||||
was_halted = jlink.halted()
|
||||
if not was_halted:
|
||||
jlink.halt()
|
||||
try:
|
||||
for off in range(0, ram_size, chunk):
|
||||
data = bytes(jlink.memory_read8(ram_base + off, min(chunk, ram_size - off)))
|
||||
idx = data.find(needle)
|
||||
if idx >= 0:
|
||||
return ram_base + off + idx
|
||||
finally:
|
||||
if not was_halted:
|
||||
if hasattr(jlink, "restart"):
|
||||
jlink.restart()
|
||||
else:
|
||||
jlink.go()
|
||||
time.sleep(0.1)
|
||||
return None
|
||||
|
||||
|
||||
def rtt_read_text(jlink, timeout_s: float = 0.2) -> str:
|
||||
deadline = time.time() + timeout_s
|
||||
chunks: list[bytes] = []
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
data = bytes(jlink.rtt_read(0, 512) or [])
|
||||
except Exception:
|
||||
data = b""
|
||||
if data:
|
||||
chunks.append(data)
|
||||
deadline = time.time() + timeout_s
|
||||
else:
|
||||
time.sleep(0.02)
|
||||
return b"".join(chunks).decode("ascii", errors="replace")
|
||||
|
||||
|
||||
def wait_for(jlink, marker: str, timeout_s: float = 30.0) -> str:
|
||||
deadline = time.time() + timeout_s
|
||||
buf = ""
|
||||
while time.time() < deadline:
|
||||
buf += rtt_read_text(jlink, 0.15)
|
||||
if marker in buf:
|
||||
return buf
|
||||
raise SystemExit(f"Timeout waiting for {marker!r}\n--- RTT ---\n{buf[-800:]}")
|
||||
|
||||
|
||||
def send_bytes(jlink, payload: bytes) -> None:
|
||||
off = 0
|
||||
while off < len(payload):
|
||||
try:
|
||||
n = jlink.rtt_write(0, list(payload[off : off + 64]))
|
||||
except Exception:
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
if n is None or n <= 0:
|
||||
time.sleep(0.02)
|
||||
continue
|
||||
off += n
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--bin", default=DEFAULT_BIN)
|
||||
parser.add_argument("--device", default="Cortex-M4")
|
||||
parser.add_argument("--no-reset", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.isfile(args.bin):
|
||||
raise SystemExit(f"missing bin: {args.bin}")
|
||||
|
||||
data = open(args.bin, "rb").read()
|
||||
size = len(data)
|
||||
if size < 16 or data[:4] != DAB_MAGIC:
|
||||
raise SystemExit(f"not a DAB bank (magic={data[:4].hex()})")
|
||||
|
||||
print(f"bin={args.bin} size={size} addr=0x{EXTFLASH_BIN3_UNIVERSAL_ADDR:08X}")
|
||||
|
||||
jlink = connect_jlink(args.device)
|
||||
try:
|
||||
cb = find_rtt_control_block(jlink)
|
||||
if cb is None:
|
||||
raise SystemExit("RTT control block not found")
|
||||
print(f"RTT CB @ 0x{cb:08X}")
|
||||
jlink.rtt_start(cb)
|
||||
time.sleep(0.3)
|
||||
_ = rtt_read_text(jlink, 0.3)
|
||||
|
||||
if not args.no_reset:
|
||||
print("hardware reset...")
|
||||
jlink.reset(halt=False)
|
||||
time.sleep(3.5)
|
||||
try:
|
||||
jlink.rtt_stop()
|
||||
except Exception:
|
||||
pass
|
||||
cb = find_rtt_control_block(jlink)
|
||||
if cb is None:
|
||||
raise SystemExit("RTT CB missing after reset")
|
||||
print(f"RTT CB @ 0x{cb:08X}")
|
||||
jlink.rtt_start(cb)
|
||||
time.sleep(0.5)
|
||||
_ = rtt_read_text(jlink, 0.5)
|
||||
|
||||
cmd = f"flash bin3 {size}\n".encode("ascii")
|
||||
send_bytes(jlink, cmd)
|
||||
print("sent flash command, waiting GO...")
|
||||
try:
|
||||
log = wait_for(jlink, "FLASH_BIN3_GO", 20.0)
|
||||
except SystemExit:
|
||||
raise SystemExit(
|
||||
"Firmware did not accept 'flash bin3'. Rebuild App with app_log.c "
|
||||
"support, or temporarily flash via: concatenate 2.bin+3.bin and "
|
||||
"'flash haitian <combined_size>'."
|
||||
)
|
||||
print(log.strip().splitlines()[-1])
|
||||
time.sleep(0.2)
|
||||
|
||||
chunk = 256
|
||||
sent = 0
|
||||
t0 = time.time()
|
||||
while sent < size:
|
||||
end = min(sent + chunk, size)
|
||||
send_bytes(jlink, data[sent:end])
|
||||
sent = end
|
||||
log = wait_for(jlink, "FLASH_ACK", 60.0)
|
||||
if "FLASH_BIN3_OK" in log:
|
||||
print(log.strip().splitlines()[-1])
|
||||
print("Done. Universal bank (3.bin) updated.")
|
||||
return
|
||||
if sent % (4 * 1024) == 0 or sent == size:
|
||||
elapsed = time.time() - t0
|
||||
print(f" {sent}/{size} ({100.0 * sent / size:.1f}%) {elapsed:.1f}s")
|
||||
|
||||
log = wait_for(jlink, "FLASH_BIN3_OK", 60.0)
|
||||
print(log.strip().splitlines()[-1])
|
||||
print("Done. Universal bank (3.bin) updated.")
|
||||
finally:
|
||||
try:
|
||||
jlink.rtt_stop()
|
||||
except Exception:
|
||||
pass
|
||||
jlink.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Generate protocol/git_user_fw_ver.h from current git HEAD + branch.
|
||||
|
||||
用户固件版本 (01 0C) 线格式: {branch}_{short6}[optional '*']
|
||||
例: develop_0aedb4 / feature-ui_57448b*
|
||||
|
||||
- short6 = git rev-parse --short=6 HEAD(小写 hex)
|
||||
- branch 清洗为 [A-Za-z0-9.-],整串(不含 dirty '*') ≤ 24
|
||||
- dirty working tree 时末尾追加 '*'
|
||||
|
||||
用法(IAR Pre-build):
|
||||
python \"$PROJ_DIR$\\..\\..\\tools\\gen_git_user_fw_ver.py\"
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
OUT = os.path.join(ROOT, "protocol", "git_user_fw_ver.h")
|
||||
BUILD_ID_MAX = 24 # without trailing dirty '*'
|
||||
|
||||
|
||||
def _git(*args: str) -> str | None:
|
||||
try:
|
||||
return subprocess.check_output(
|
||||
["git", *args],
|
||||
cwd=ROOT,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
).strip()
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def sanitize_branch(name: str) -> str:
|
||||
if not name or name == "HEAD":
|
||||
return "DETACHED"
|
||||
# feature/foo → feature-foo;去掉其它非法字符
|
||||
s = name.replace("/", "-")
|
||||
s = re.sub(r"[^A-Za-z0-9.-]+", "-", s)
|
||||
s = re.sub(r"-{2,}", "-", s).strip("-.")
|
||||
return s or "DETACHED"
|
||||
|
||||
|
||||
def make_build_id(branch: str, short6: str) -> str:
|
||||
"""Ensure '{branch}_{short6}' length ≤ BUILD_ID_MAX; keep commit suffix intact."""
|
||||
short6 = short6.lower()[:6].ljust(6, "0")
|
||||
suffix = "_" + short6
|
||||
max_br = BUILD_ID_MAX - len(suffix)
|
||||
if max_br < 1:
|
||||
return short6[:BUILD_ID_MAX]
|
||||
br = branch[:max_br]
|
||||
return br + suffix
|
||||
|
||||
|
||||
def main() -> int:
|
||||
full = _git("rev-parse", "HEAD") or ("0" * 40)
|
||||
if len(full) < 6 or not all(c in "0123456789abcdefABCDEF" for c in full):
|
||||
full = "0" * 40
|
||||
full = full.lower()
|
||||
short6 = full[:6]
|
||||
|
||||
br_raw = _git("rev-parse", "--abbrev-ref", "HEAD") or "DETACHED"
|
||||
branch = sanitize_branch(br_raw)
|
||||
|
||||
dirty = False
|
||||
st = _git("status", "--porcelain")
|
||||
if st:
|
||||
dirty = True
|
||||
|
||||
build_id = make_build_id(branch, short6)
|
||||
wire = build_id + ("*" if dirty else "")
|
||||
# C string escape
|
||||
wire_c = wire.replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
text = f"""/* Auto-generated by tools/gen_git_user_fw_ver.py — do not edit.
|
||||
* git HEAD{' (dirty)' if dirty else ''}: {full}
|
||||
* branch: {br_raw} → {branch}
|
||||
* 01 0C wire: {wire}
|
||||
*/
|
||||
#ifndef GIT_USER_FW_VER_H
|
||||
#define GIT_USER_FW_VER_H
|
||||
|
||||
#define GIT_COMMIT_ID_FULL "{full}"
|
||||
#define GIT_BRANCH_NAME "{branch}"
|
||||
#define GIT_COMMIT_SHORT6 "{short6}"
|
||||
#define GIT_DIRTY ({1 if dirty else 0})
|
||||
#define GIT_BUILD_ID "{wire_c}"
|
||||
#define GIT_BUILD_ID_LEN {len(wire)}u
|
||||
|
||||
#endif /* GIT_USER_FW_VER_H */
|
||||
"""
|
||||
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
||||
old = ""
|
||||
if os.path.isfile(OUT):
|
||||
with open(OUT, "r", encoding="utf-8") as f:
|
||||
old = f.read()
|
||||
if old != text:
|
||||
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(text)
|
||||
print(f"gen_git_user_fw_ver: wrote {OUT} ({wire})")
|
||||
else:
|
||||
print(f"gen_git_user_fw_ver: up-to-date ({wire})")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -61,8 +61,14 @@ def rgba_to_rgb565(im):
|
|||
|
||||
|
||||
def rgba_to_rgb565_opaque(im):
|
||||
"""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) ----------------
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
if cursor < force_off:
|
||||
pad = force_off - cursor
|
||||
blobs.append(b"\xFF" * pad)
|
||||
cursor = force_off
|
||||
rows.append((f"(pad)", force_off - pad, pad, "gap fill 0xFF"))
|
||||
if cursor > force_off:
|
||||
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
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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 本版未重排。",
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Bottom nav: 0909 enlarged icons + original design text strips, fixed baseline."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
PROJ = Path(__file__).resolve().parents[1]
|
||||
ROOT = PROJ.parent.parent
|
||||
SRC_DIR = next(
|
||||
p
|
||||
for p in (ROOT / "Doc" / "UI" / "K1标准界面图 0904").iterdir()
|
||||
if p.is_dir() and "0909" in p.name
|
||||
)
|
||||
ASSETS = PROJ / "tools" / "assets_0902" / "03_底部图标"
|
||||
DOC_ASSETS = ROOT / "Doc" / "UI" / "k1_ui_png 0902" / "03_底部图标"
|
||||
OUT_C = PROJ / "device" / "LCD_ILI9341" / "Drv_ILI9341_Lcd_Image_UI0902.c"
|
||||
OUT_H = PROJ / "device" / "LCD_ILI9341" / "Drv_ILI9341_Lcd_Image_UI0902.h"
|
||||
|
||||
ROW_BG = (36, 43, 58)
|
||||
CANVAS_H = 32
|
||||
ICON_AREA_H = 20
|
||||
TEXT_TOP = 22 # all labels share this top row
|
||||
|
||||
# png_idx, src_glyph_num, symbol_base, width
|
||||
MAPPING = [
|
||||
(10, 21, "gImage_UI0902_TabSetting_Sel", 40),
|
||||
(11, 23, "gImage_UI0902_TabSetting_Not", 40),
|
||||
(12, 25, "gImage_UI0902_TabMixer_Sel", 40),
|
||||
(13, 27, "gImage_UI0902_TabMixer_Not", 22),
|
||||
(14, 20, "gImage_UI0902_TabMode_Sel", 40),
|
||||
(15, 22, "gImage_UI0902_TabMode_Not", 40),
|
||||
(16, 24, "gImage_UI0902_TabBack_Sel", 40),
|
||||
(17, 26, "gImage_UI0902_TabBack_Not", 22),
|
||||
]
|
||||
|
||||
|
||||
def git_png(rel: str) -> Image.Image:
|
||||
data = subprocess.check_output(["git", "show", f"HEAD:{rel}"], cwd=PROJ)
|
||||
return Image.open(BytesIO(data)).convert("RGBA")
|
||||
|
||||
|
||||
def clear_bg(im: Image.Image) -> Image.Image:
|
||||
im = im.convert("RGBA")
|
||||
px = im.load()
|
||||
for y in range(im.height):
|
||||
for x in range(im.width):
|
||||
r, g, b, a = px[x, y]
|
||||
if a < 40 or (r < 12 and g < 12 and b < 12):
|
||||
px[x, y] = (0, 0, 0, 0)
|
||||
return im
|
||||
|
||||
|
||||
def row_has_ink(im: Image.Image, y: int) -> bool:
|
||||
w = im.width
|
||||
for x in range(w):
|
||||
r, g, b, a = im.getpixel((x, y))
|
||||
if a > 40 and (r + g + b) > 40:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def extract_text(im: Image.Image) -> Image.Image:
|
||||
"""Crop label band below the icon/gap from original composite."""
|
||||
im = clear_bg(im)
|
||||
h = im.height
|
||||
# find last empty gap row in upper half, then text starts after
|
||||
empty = [y for y in range(h) if not row_has_ink(im, y)]
|
||||
# text region: from first non-empty after mid-gap
|
||||
mid_empties = [y for y in empty if 10 <= y <= 20]
|
||||
if mid_empties:
|
||||
text_y0 = mid_empties[-1] + 1
|
||||
else:
|
||||
text_y0 = h // 2 + 2
|
||||
while text_y0 < h and not row_has_ink(im, text_y0):
|
||||
text_y0 += 1
|
||||
text_y1 = h - 1
|
||||
while text_y1 > text_y0 and not row_has_ink(im, text_y1):
|
||||
text_y1 -= 1
|
||||
band = im.crop((0, text_y0, im.width, text_y1 + 1))
|
||||
# trim horizontal transparent
|
||||
bbox = band.getbbox()
|
||||
if bbox:
|
||||
band = band.crop(bbox)
|
||||
return clear_bg(band)
|
||||
|
||||
|
||||
def fit_icon(im: Image.Image) -> Image.Image:
|
||||
im = clear_bg(im)
|
||||
nw, nh = im.size
|
||||
if nw > 24 or nh > ICON_AREA_H:
|
||||
scale = min(24 / nw, ICON_AREA_H / nh)
|
||||
nw = max(1, int(round(nw * scale)))
|
||||
nh = max(1, int(round(nh * scale)))
|
||||
im = clear_bg(im.resize((nw, nh), Image.Resampling.NEAREST))
|
||||
nw, nh = im.size
|
||||
return im
|
||||
|
||||
|
||||
def rgba_to_rgb565(im: Image.Image) -> tuple[bytes, int, int]:
|
||||
im = im.convert("RGBA")
|
||||
px = im.load()
|
||||
w, h = im.size
|
||||
out = bytearray(w * h * 2)
|
||||
i = 0
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
r, g, b, a = px[x, y]
|
||||
if a < 40 or (r < 12 and g < 12 and b < 12):
|
||||
out[i] = out[i + 1] = 0
|
||||
elif (
|
||||
abs(r - ROW_BG[0]) <= 18
|
||||
and abs(g - ROW_BG[1]) <= 18
|
||||
and abs(b - ROW_BG[2]) <= 18
|
||||
):
|
||||
out[i] = out[i + 1] = 0
|
||||
else:
|
||||
r = (r * a) // 255
|
||||
g = (g * a) // 255
|
||||
b = (b * a) // 255
|
||||
v = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
|
||||
out[i] = (v >> 8) & 0xFF
|
||||
out[i + 1] = v & 0xFF
|
||||
i += 2
|
||||
return bytes(out), w, h
|
||||
|
||||
|
||||
def emit_c_array(name: str, data: bytes, w: int, h: int) -> str:
|
||||
lines = [f"const unsigned char {name}[{len(data)}] = {{ /* {w}x{h} RGB565 BE */"]
|
||||
for i in range(0, len(data), 16):
|
||||
lines.append(",".join(f"0x{b:02X}" for b in data[i : i + 16]) + ",")
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def replace_c_array(text: str, old_name: str, new_block: str) -> str:
|
||||
pat = re.compile(
|
||||
rf"const unsigned char {re.escape(old_name)}\[\d+\] = \{{.*?\n\}};\n?",
|
||||
re.S,
|
||||
)
|
||||
if not pat.search(text):
|
||||
raise SystemExit(f"array not found: {old_name}")
|
||||
return pat.sub(new_block, text, count=1)
|
||||
|
||||
|
||||
def replace_h_decl(text: str, old_name: str, new_name: str, nbytes: int) -> str:
|
||||
pat = re.compile(rf"extern const unsigned char {re.escape(old_name)}\[\d+\];")
|
||||
if not pat.search(text):
|
||||
raise SystemExit(f"decl not found: {old_name}")
|
||||
return pat.sub(
|
||||
f"extern const unsigned char {new_name}[{nbytes}];", text, count=1
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
by_num = {int(f.stem.rsplit("-", 1)[-1]): f for f in SRC_DIR.glob("*.png")}
|
||||
c_text = OUT_C.read_text(encoding="utf-8")
|
||||
h_text = OUT_H.read_text(encoding="utf-8")
|
||||
sizes: dict[int, tuple[int, int]] = {}
|
||||
|
||||
for out_idx, src_num, base, width in MAPPING:
|
||||
icon = fit_icon(Image.open(by_num[src_num]))
|
||||
old = git_png(f"tools/assets_0902/03_底部图标/底部图标-{out_idx}.png")
|
||||
text = extract_text(old)
|
||||
|
||||
# widen canvas if text wider
|
||||
cw = max(width, icon.width, text.width)
|
||||
canvas = Image.new("RGBA", (cw, CANVAS_H), (0, 0, 0, 0))
|
||||
iy = (ICON_AREA_H - icon.height) // 2
|
||||
canvas.paste(icon, ((cw - icon.width) // 2, max(0, iy)), icon)
|
||||
# pin text BOTTOM to same row so labels sit on one baseline
|
||||
text_bottom = CANVAS_H - 1
|
||||
ty = text_bottom - text.height + 1
|
||||
if ty < TEXT_TOP:
|
||||
ty = TEXT_TOP
|
||||
canvas.paste(text, ((cw - text.width) // 2, ty), text)
|
||||
|
||||
out = ASSETS / f"底部图标-{out_idx}.png"
|
||||
canvas.save(out)
|
||||
if DOC_ASSETS.exists():
|
||||
canvas.save(DOC_ASSETS / f"底部图标-{out_idx}.png")
|
||||
|
||||
data, w, h = rgba_to_rgb565(canvas)
|
||||
new_name = f"{base}_{w}x{h}"
|
||||
m = re.search(rf"{re.escape(base)}_\d+x\d+", c_text)
|
||||
if not m:
|
||||
raise SystemExit(f"cannot locate {base}")
|
||||
old_name = m.group(0)
|
||||
c_text = replace_c_array(c_text, old_name, emit_c_array(new_name, data, w, h))
|
||||
h_text = replace_h_decl(h_text, old_name, new_name, len(data))
|
||||
sizes[out_idx] = (w, h)
|
||||
print(f"{out_idx}: {w}x{h} text={text.size} -> {new_name}")
|
||||
|
||||
OUT_C.write_text(c_text, encoding="utf-8")
|
||||
OUT_H.write_text(h_text, encoding="utf-8")
|
||||
print("SIZES", sizes)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,447 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
test_protocol_app_sim.py — 模拟手机 App 对吉他固件做全协议串口测试
|
||||
|
||||
协议来源: Doc/指令测试.docx (= 指令测试0907.docx)
|
||||
通道: UART4 (BLE 桥), 115200 8N1; 帧格式 F0 60 <cmd1> <cmd2> [data...] F7
|
||||
设备主动上报为 F0 51 ... F7 (测试时跳过, 不计为响应)
|
||||
|
||||
用法:
|
||||
python test_protocol_app_sim.py --port COM5 # 跑全部安全用例
|
||||
python test_protocol_app_sim.py --port COM5 --allow-poweroff# 含 05 00 关机用例
|
||||
python test_protocol_app_sim.py --selftest # 无硬件自检(帧编解码)
|
||||
|
||||
依赖: pyserial (pip install pyserial)
|
||||
退出码: 0 = 全部通过, 1 = 有 FAIL
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import serial
|
||||
except ImportError:
|
||||
serial = None # --selftest 不需要 pyserial
|
||||
|
||||
HEAD, TAIL, APP_ID, DEV_ID = 0xF0, 0xF7, 0x60, 0x51
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- frame codec
|
||||
def build(*payload):
|
||||
"""App -> 设备: F0 60 <payload...> F7"""
|
||||
return bytes([HEAD, APP_ID, *payload, TAIL])
|
||||
|
||||
|
||||
class FrameParser:
|
||||
"""字节流 -> 完整帧; 只收 F0...F7, 其余丢弃"""
|
||||
|
||||
def __init__(self):
|
||||
self.buf = bytearray()
|
||||
|
||||
def feed(self, data: bytes):
|
||||
"""返回本轮解析出的完整帧列表"""
|
||||
out = []
|
||||
for b in data:
|
||||
if not self.buf:
|
||||
if b == HEAD:
|
||||
self.buf.append(b)
|
||||
continue
|
||||
# BLE-MIDI 粘包时可能再次出现 F0;重新同步,避免 F0 F0 60…
|
||||
if b == HEAD:
|
||||
self.buf = bytearray([HEAD])
|
||||
continue
|
||||
self.buf.append(b)
|
||||
if b == TAIL or len(self.buf) >= 64:
|
||||
if b == TAIL and len(self.buf) >= 4:
|
||||
out.append(bytes(self.buf))
|
||||
self.buf.clear()
|
||||
return out
|
||||
|
||||
|
||||
def hexs(b):
|
||||
return " ".join(f"{x:02X}" for x in b) if b else "(none)"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- serial wrap
|
||||
class AppSim:
|
||||
def __init__(self, port, baud=115200, timeout=0.15):
|
||||
self.ser = serial.Serial(port, baud, bytesize=8, parity="N",
|
||||
stopbits=1, timeout=timeout)
|
||||
self.parser = FrameParser()
|
||||
self.pending = []
|
||||
|
||||
def close(self):
|
||||
self.ser.close()
|
||||
|
||||
def _pump(self):
|
||||
data = self.ser.read(256)
|
||||
if data:
|
||||
self.pending.extend(self.parser.feed(data))
|
||||
|
||||
def send(self, frame: bytes):
|
||||
self.ser.write(frame)
|
||||
self.ser.flush()
|
||||
|
||||
def read_frame(self, timeout=1.0, want_dev=False):
|
||||
"""读一帧; 默认跳过设备主动上报(0x51), want_dev=True 时只要 0x51"""
|
||||
deadline = time.monotonic() + timeout
|
||||
while True:
|
||||
self._pump()
|
||||
for i, f in enumerate(self.pending):
|
||||
is_dev = len(f) > 1 and f[1] == DEV_ID
|
||||
if is_dev == want_dev:
|
||||
return self.pending.pop(i)
|
||||
self.pending.clear() # 丢掉方向不符的帧
|
||||
if time.monotonic() >= deadline:
|
||||
return None
|
||||
time.sleep(0.01)
|
||||
|
||||
def drain(self, quiet=0.2):
|
||||
while self.read_frame(timeout=quiet) is not None:
|
||||
pass
|
||||
|
||||
def query(self, frame: bytes, timeout=1.0):
|
||||
self.drain()
|
||||
self.send(frame)
|
||||
return self.read_frame(timeout=timeout)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- test engine
|
||||
class Results:
|
||||
def __init__(self):
|
||||
self.rows = []
|
||||
|
||||
def add(self, name, status, detail=""):
|
||||
self.rows.append((name, status, detail))
|
||||
print(f"[{status:>4}] {name}" + (f" | {detail}" if detail else ""))
|
||||
|
||||
def summary(self):
|
||||
n = {s: sum(1 for r in self.rows if r[1] == s)
|
||||
for s in ("PASS", "FAIL", "SKIP", "SENT")}
|
||||
print("\n===== 汇总 =====")
|
||||
for k in ("PASS", "FAIL", "SKIP", "SENT"):
|
||||
print(f"{k:>5}: {n[k]}")
|
||||
for name, status, detail in self.rows:
|
||||
if status == "FAIL":
|
||||
print(f" FAIL: {name} | {detail}")
|
||||
return 1 if n["FAIL"] else 0
|
||||
|
||||
|
||||
def expect(resp, prefix, length=None):
|
||||
"""校验响应帧头/命令字/长度"""
|
||||
if resp is None:
|
||||
return False, "timeout, no reply"
|
||||
if len(resp) < 5 or resp[0] != HEAD or resp[-1] != TAIL:
|
||||
return False, f"bad frame: {hexs(resp)}"
|
||||
if tuple(resp[1:1 + len(prefix)]) != tuple(prefix):
|
||||
return False, f"prefix mismatch: {hexs(resp)}"
|
||||
if length is not None and len(resp) != length:
|
||||
return False, f"len {len(resp)} != {length}: {hexs(resp)}"
|
||||
return True, hexs(resp)
|
||||
|
||||
|
||||
def fmt_ver4(payload: bytes, *, kind: str = "semver") -> str:
|
||||
"""解码版本字节为可读串。
|
||||
|
||||
kind:
|
||||
semver — 前 3 字节 major.minor.patch(01 03/0C)
|
||||
date — YY.M.D 日期(01 04 音源 / 01 05 UI),并展开为 20YY-MM-DD
|
||||
"""
|
||||
if len(payload) != 4:
|
||||
return f"ver_raw={payload!r}"
|
||||
a, b, c, _d = payload
|
||||
if kind == "date":
|
||||
return f"ver={a}.{b}.{c} date=20{a:02d}-{b:02d}-{c:02d}"
|
||||
return f"ver={a}.{b}.{c}"
|
||||
|
||||
|
||||
def run_tests(sim, allow_poweroff=False):
|
||||
R = Results()
|
||||
|
||||
# ---------------- 1. 设备信息 0x01 ----------------
|
||||
r = sim.query(build(0x01, 0x01))
|
||||
ok, d = expect(r, (0x60, 0x01, 0x01), 6)
|
||||
R.add("01 01 连接设备", "PASS" if ok else "FAIL", d)
|
||||
|
||||
r = sim.query(build(0x01, 0x02))
|
||||
ok, d = expect(r, (0x60, 0x01, 0x02))
|
||||
if ok:
|
||||
name = bytes(r[4:-1])
|
||||
ok = all(0x20 <= c < 0x7F for c in name)
|
||||
d += f" name={name!r}"
|
||||
R.add("01 02 设备名", "PASS" if ok else "FAIL", d)
|
||||
|
||||
for sub, name, kind in (
|
||||
(0x03, "固件主版本", "semver"),
|
||||
(0x04, "音源版本", "date"),
|
||||
(0x05, "UI版本", "date"),
|
||||
):
|
||||
r = sim.query(build(0x01, sub))
|
||||
ok, d = expect(r, (0x60, 0x01, sub), 9)
|
||||
if ok:
|
||||
d += f" {fmt_ver4(bytes(r[4:-1]), kind=kind)}"
|
||||
R.add(f"01 {sub:02X} {name}", "PASS" if ok else "FAIL", d)
|
||||
|
||||
# 01 0C:{branch}_{short6}[optional '*'],如 develop_0aedb4
|
||||
r = sim.query(build(0x01, 0x0C))
|
||||
ok, d = expect(r, (0x60, 0x01, 0x0C))
|
||||
if ok:
|
||||
raw = bytes(r[4:-1])
|
||||
ok = (
|
||||
3 <= len(raw) <= 25
|
||||
and all(0x20 <= c < 0x7F for c in raw)
|
||||
and b"_" in raw.rstrip(b"*")
|
||||
)
|
||||
build_s = raw.decode("ascii", "replace")
|
||||
dirty = build_s.endswith("*")
|
||||
core = build_s[:-1] if dirty else build_s
|
||||
branch, _, short = core.rpartition("_")
|
||||
extra = f" build={build_s!r}"
|
||||
if len(short) == 6 and all(c in "0123456789abcdef" for c in short.lower()):
|
||||
extra += f" branch={branch!r} commit={short.lower()}"
|
||||
if dirty:
|
||||
extra += " dirty=1"
|
||||
d += extra
|
||||
R.add("01 0C 用户固件版本", "PASS" if ok else "FAIL", d)
|
||||
|
||||
r = sim.query(build(0x01, 0x06))
|
||||
ok, d = expect(r, (0x60, 0x01, 0x06), 13)
|
||||
R.add("01 06 其他信息", "PASS" if ok else "FAIL", d)
|
||||
|
||||
r = sim.query(build(0x01, 0x07))
|
||||
ok, d = expect(r, (0x60, 0x01, 0x07), 29) # 12B UID → 24 hex ASCII
|
||||
if ok:
|
||||
hx = bytes(r[4:-1])
|
||||
ok = len(hx) == 24 and all(c in b"0123456789ABCDEF" for c in hx)
|
||||
d += f" uid_hex={hx.decode('ascii', 'replace')}"
|
||||
R.add("01 07 设备编码(UID)", "PASS" if ok else "FAIL", d)
|
||||
|
||||
# 01 0F:BLE-MIDI 安全子命令(原 01 FF 的 0xFF 非法出现在 SysEx 数据中)
|
||||
r = sim.query(build(0x01, 0x0F))
|
||||
ok, d = expect(r, (0x60, 0x01, 0x0F))
|
||||
if ok:
|
||||
code = bytes(r[4:-1])
|
||||
ok = len(code) > 0 and all(0x20 <= c < 0x7F for c in code)
|
||||
d += f" code={code!r}"
|
||||
R.add("01 0F 固件编码", "PASS" if ok else "FAIL", d)
|
||||
|
||||
# 自动关机: 读 -> 设15 -> 读验证 -> 恢复原值
|
||||
r0 = sim.query(build(0x01, 0x0A))
|
||||
ok, d = expect(r0, (0x60, 0x01, 0x0A), 6)
|
||||
R.add("01 0A 自动关机(读)", "PASS" if ok else "FAIL", d)
|
||||
if ok:
|
||||
orig = r0[4]
|
||||
r = sim.query(build(0x01, 0x11, 15))
|
||||
ok, d = expect(r, (0x60, 0x01, 0x11, 15), 6)
|
||||
R.add("01 11 自动关机(设15)", "PASS" if ok else "FAIL", d)
|
||||
r = sim.query(build(0x01, 0x0A))
|
||||
ok, d = expect(r, (0x60, 0x01, 0x0A, 15), 6)
|
||||
R.add("01 0A 回读=15", "PASS" if ok else "FAIL", d)
|
||||
sim.query(build(0x01, 0x11, orig)) # restore
|
||||
|
||||
r = sim.query(build(0x01, 0x00))
|
||||
ok, d = expect(r, (0x60, 0x01, 0x00), 6)
|
||||
R.add("01 00 断开设备", "PASS" if ok else "FAIL", d)
|
||||
sim.query(build(0x01, 0x01)) # 重新连接, 便于后续用例
|
||||
|
||||
# ---------------- 2. 和弦映射 0x02 ----------------
|
||||
r = sim.query(build(0x02, 0x01))
|
||||
ok, d = expect(r, (0x60, 0x02, 0x01), 47)
|
||||
R.add("02 01 读和弦映射表", "PASS" if ok else "FAIL", d)
|
||||
cur_map = bytes(r[4:-1]) if ok else None # 21B pitch + 21B chord
|
||||
|
||||
if cur_map:
|
||||
key = 1
|
||||
orig_pitch, orig_chord = cur_map[key - 1], cur_map[21 + key - 1]
|
||||
|
||||
sim.send(build(0x02, 0x02, key - 1, 0x02)) # pitch: 升位
|
||||
time.sleep(0.1)
|
||||
r = sim.query(build(0x02, 0x01))
|
||||
ok = r is not None and len(r) == 47 and r[3] == 0x01 and r[4 + key - 1] == 0x02
|
||||
R.add("02 02 Pitch偏移(写+读回)", "PASS" if ok else "FAIL", hexs(r))
|
||||
sim.send(build(0x02, 0x02, key - 1, orig_pitch)) # restore
|
||||
|
||||
sim.send(build(0x02, 0x03, key - 1, 0x08)) # chord: 小三
|
||||
time.sleep(0.1)
|
||||
r = sim.query(build(0x02, 0x01))
|
||||
ok = r is not None and len(r) == 47 and r[4 + 21 + key - 1] == 0x08
|
||||
R.add("02 03 Chord偏移(写+读回)", "PASS" if ok else "FAIL", hexs(r))
|
||||
sim.send(build(0x02, 0x03, key - 1, orig_chord)) # restore
|
||||
time.sleep(0.1)
|
||||
|
||||
# 02 04 整表写入(回写当前表) -> 设备应回 02 01 格式整表
|
||||
r = sim.query(build(0x02, 0x04, *cur_map), timeout=2.0)
|
||||
ok, d = expect(r, (0x60, 0x02, 0x01), 47)
|
||||
R.add("02 04 整表写入(回读帧)", "PASS" if ok else "FAIL", d)
|
||||
r2 = sim.query(build(0x02, 0x01))
|
||||
ok = r2 is not None and bytes(r2[4:-1]) == cur_map
|
||||
R.add("02 04 写入后整表一致", "PASS" if ok else "FAIL", hexs(r2))
|
||||
else:
|
||||
for n in ("02 02 Pitch偏移", "02 03 Chord偏移", "02 04 整表写入"):
|
||||
R.add(n, "SKIP", "读映射表失败, 级联跳过")
|
||||
|
||||
# ---------------- 3. 吉他参数 0x03 ----------------
|
||||
r = sim.query(build(0x03, 0x04, 0x00))
|
||||
ok, d = expect(r, (0x60, 0x03, 0x04, 0x00), 9)
|
||||
R.add("03 04 读当前节奏风格", "PASS" if ok else "FAIL", d)
|
||||
saved_style = bytes(r[5:8]) if ok else None
|
||||
|
||||
# 开机特例: F0 60 03 04 01 00 00 00 F7 -> F0 60 03 04 01 <bpm> F7 + F0 51 03 <hi> <lo> F7
|
||||
r = sim.query(build(0x03, 0x04, 0x01, 0x00, 0x00, 0x00))
|
||||
ok, d = expect(r, (0x60, 0x03, 0x04, 0x01), 7)
|
||||
R.add("03 04 开机特例(读风格#0)", "PASS" if ok else "FAIL", d)
|
||||
r51 = sim.read_frame(timeout=1.0, want_dev=True)
|
||||
ok51 = (r51 is not None and len(r51) == 6
|
||||
and tuple(r51[:3]) == (HEAD, DEV_ID, 0x03))
|
||||
R.add("03 04 开机特例(F0 51 BPM上报)",
|
||||
"PASS" if ok51 else "FAIL", hexs(r51))
|
||||
|
||||
r = sim.query(build(0x03, 0x04, 0x01, 0x01, 0x00, 0x00))
|
||||
ok, d = expect(r, (0x60, 0x03, 0x04, 0x01), 7)
|
||||
R.add("03 04 设用户风格#0", "PASS" if ok else "FAIL", d)
|
||||
|
||||
# 用户风格分页: 回复 F0 60 03 04 02 <cntHi> <cntLo> [<idHi> <idLo> <code4B> ...] F7
|
||||
r = sim.query(build(0x03, 0x04, 0x02, 0x00, 0x00, 0x00, 0x07))
|
||||
ok, d = expect(r, (0x60, 0x03, 0x04, 0x02))
|
||||
if ok:
|
||||
cnt = r[5] * 128 + r[6]
|
||||
ok = len(r) == 7 + cnt * 6 + 1
|
||||
d += f" cnt={cnt}"
|
||||
R.add("03 04 用户风格分页", "PASS" if ok else "FAIL", d)
|
||||
|
||||
# 用户风格总数: 回复 F0 60 03 04 00 <total 3B BE> F7
|
||||
r = sim.query(build(0x03, 0x04, 0x03))
|
||||
ok, d = expect(r, (0x60, 0x03, 0x04, 0x00), 9)
|
||||
if ok:
|
||||
d += f" total={(r[5] << 16) | (r[6] << 8) | r[7]}"
|
||||
R.add("03 04 用户风格总数", "PASS" if ok else "FAIL", d)
|
||||
|
||||
if saved_style:
|
||||
sim.query(build(0x03, 0x04, 0x01, *saved_style)) # restore style
|
||||
|
||||
# 弦音色: 读 -> 写 -> 读 -> 恢复
|
||||
r0 = sim.query(build(0x03, 0x05, 0x00))
|
||||
ok, d = expect(r0, (0x60, 0x03, 0x05, 0x00), 8)
|
||||
R.add("03 05 读弦音色", "PASS" if ok else "FAIL", d)
|
||||
if ok:
|
||||
orig = r0[6]
|
||||
new = (orig + 1) % 5
|
||||
sim.send(build(0x03, 0x05, 0x01, 0x00, new))
|
||||
time.sleep(0.1)
|
||||
r = sim.query(build(0x03, 0x05, 0x00))
|
||||
ok = r is not None and len(r) == 8 and r[6] == new
|
||||
R.add("03 05 写弦音色(写+读回)", "PASS" if ok else "FAIL", hexs(r))
|
||||
sim.send(build(0x03, 0x05, 0x01, 0x00, orig)) # restore
|
||||
|
||||
# BPM: 读 -> 写120 -> 读 -> 恢复
|
||||
r0 = sim.query(build(0x03, 0x06, 0x00))
|
||||
ok, d = expect(r0, (0x60, 0x03, 0x06, 0x00), 8)
|
||||
R.add("03 06 读BPM", "PASS" if ok else "FAIL", d)
|
||||
if ok:
|
||||
orig = r0[5] * 128 + r0[6]
|
||||
sim.send(build(0x03, 0x06, 0x01, 120 // 128, 120 % 128))
|
||||
time.sleep(0.1)
|
||||
r = sim.query(build(0x03, 0x06, 0x00))
|
||||
ok = r is not None and len(r) == 8 and (r[5] * 128 + r[6]) == 120
|
||||
R.add("03 06 写BPM=120(写+读回)", "PASS" if ok else "FAIL", hexs(r))
|
||||
sim.send(build(0x03, 0x06, 0x01, orig // 128, orig % 128)) # restore
|
||||
|
||||
# 移调: 读 -> 写 -> 读 -> 恢复 (doc: F0 60 03 07 00 <val> F7, 7 bytes)
|
||||
r0 = sim.query(build(0x03, 0x07, 0x00))
|
||||
ok, d = expect(r0, (0x60, 0x03, 0x07, 0x00), 7)
|
||||
R.add("03 07 读移调", "PASS" if ok else "FAIL", d)
|
||||
if ok:
|
||||
orig = r0[5]
|
||||
new = (orig + 1) % 12
|
||||
sim.send(build(0x03, 0x07, 0x01, new))
|
||||
time.sleep(0.1)
|
||||
r = sim.query(build(0x03, 0x07, 0x00))
|
||||
ok = r is not None and len(r) == 7 and r[5] == new
|
||||
R.add("03 07 写移调(写+读回)", "PASS" if ok else "FAIL", hexs(r))
|
||||
sim.send(build(0x03, 0x07, 0x01, orig)) # restore
|
||||
|
||||
# ---------------- 4. LED / 播放控制 0x04 (无应答, 仅下发) ----------------
|
||||
for i in range(7):
|
||||
sim.drain()
|
||||
sim.send(build(0x04, i, 0x02, 0x02))
|
||||
r = sim.read_frame(timeout=0.3)
|
||||
R.add(f"04 0{i} 点亮LED{i + 1}", "SENT" if r is None else "PASS",
|
||||
"" if r is None else f"unexpected reply {hexs(r)}")
|
||||
time.sleep(0.05)
|
||||
|
||||
sim.drain()
|
||||
sim.send(build(0x04, 0x07, 0x00, 0x00))
|
||||
R.add("04 07 End/结束播放", "SENT", "无应答属正常")
|
||||
|
||||
# ---------------- 6. 段落跳转 0x06 (无应答, 仅下发) ----------------
|
||||
for sub, name in ((0x01, "前奏"), (0x02, "间奏"), (0x03, "尾奏"),
|
||||
(0x05, "A段"), (0x06, "B段"), (0x07, "C段"), (0x08, "D段")):
|
||||
sim.drain()
|
||||
sim.send(build(0x06, sub, 0x00))
|
||||
R.add(f"06 {sub:02X} {name}", "SENT", "无应答属正常")
|
||||
time.sleep(0.05)
|
||||
|
||||
# ---------------- 5. 复位/关机 0x05 ----------------
|
||||
if allow_poweroff:
|
||||
time.sleep(0.3)
|
||||
# 禁止 query 自动重发:第二次会打在已断电的 BT 上
|
||||
sim.drain()
|
||||
sim.send(build(0x05, 0x00))
|
||||
r = sim.read_frame(timeout=3.0)
|
||||
ok, d = expect(r, (0x60, 0x05, 0x00), 6)
|
||||
if not ok and r is None and hasattr(sim, "is_connected"):
|
||||
# ACK 可能被软关机掐断 BT 抢走;链路断开也视为关机已执行
|
||||
time.sleep(0.5)
|
||||
if not sim.is_connected():
|
||||
ok, d = True, "no ACK but BLE link dropped (soft-off likely)"
|
||||
R.add("05 00 复位/关机", "PASS" if ok else "FAIL", d + " (设备将软关机)")
|
||||
else:
|
||||
R.add("05 00 复位/关机", "SKIP", "需 --allow-poweroff")
|
||||
|
||||
return R
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- selftest
|
||||
def selftest():
|
||||
R = Results()
|
||||
f = build(0x03, 0x06, 0x01, 0x00, 0x40)
|
||||
R.add("build frame", "PASS" if f == bytes.fromhex("F060030601004 0F7".replace(" ", "")) else "FAIL", hexs(f))
|
||||
|
||||
p = FrameParser()
|
||||
out = p.feed(bytes.fromhex("AA F0 60 01 01")) + p.feed(bytes.fromhex("01 F7"))
|
||||
R.add("parser chunked", "PASS" if out == [bytes.fromhex("F0600101 01F7".replace(" ", ""))] else "FAIL",
|
||||
str([hexs(x) for x in out]))
|
||||
|
||||
p = FrameParser()
|
||||
out = p.feed(bytes.fromhex("F0 51 05 0A F7 F0 60 01 01 01 F7".replace(" ", "")))
|
||||
R.add("parser two frames", "PASS" if len(out) == 2 else "FAIL", str(len(out)))
|
||||
return R.summary()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="模拟手机App的吉他协议全量测试")
|
||||
ap.add_argument("--port", help="串口, 如 COM5 (UART4 115200 8N1)")
|
||||
ap.add_argument("--baud", type=int, default=115200)
|
||||
ap.add_argument("--allow-poweroff", action="store_true",
|
||||
help="允许执行 05 00 关机用例")
|
||||
ap.add_argument("--selftest", action="store_true", help="无硬件自检")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.selftest:
|
||||
sys.exit(selftest())
|
||||
if not args.port:
|
||||
ap.error("需要 --port (或用 --selftest)")
|
||||
if serial is None:
|
||||
sys.exit("缺少 pyserial: pip install pyserial")
|
||||
|
||||
sim = AppSim(args.port, args.baud)
|
||||
print(f"=== 协议测试开始 {args.port}@{args.baud} ===")
|
||||
try:
|
||||
code = run_tests(sim, allow_poweroff=args.allow_poweroff).summary()
|
||||
finally:
|
||||
sim.close()
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,495 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
test_protocol_ble.py — 通过笔记本蓝牙对吉他做全协议测试并出报告
|
||||
|
||||
协议来源: Doc/指令测试.docx
|
||||
链路: 笔记本 BLE <-> "Smart Guitar MIDI" <-> 吉他 UART4
|
||||
App->设备: F0 60 ... F7 ; 设备主动上报: F0 51 ... F7
|
||||
|
||||
GATT:
|
||||
BLE-MIDI 03B80E5A-... / 7772E5DB-... (framed SysEx)
|
||||
自定义串口 e49a25f8-... / e49a25e0(写) + e49a28e1(通知) (raw SysEx)
|
||||
|
||||
用法:
|
||||
python test_protocol_ble.py
|
||||
python test_protocol_ble.py --transport midi
|
||||
python test_protocol_ble.py --transport uart
|
||||
python test_protocol_ble.py --unpair
|
||||
python test_protocol_ble.py --smoke
|
||||
python test_protocol_ble.py --selftest
|
||||
|
||||
依赖: bleak (pip install bleak)
|
||||
报告: Doc/reports/ble_sysex_<时间戳>.md
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import datetime
|
||||
import os
|
||||
import queue
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from test_protocol_app_sim import ( # noqa: E402
|
||||
HEAD, DEV_ID, build, FrameParser, Results, run_tests, hexs)
|
||||
|
||||
MIDI_SERVICE = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"
|
||||
MIDI_CHAR = "7772E5DB-3868-4112-A1A9-F2669D106BF3"
|
||||
UART_SERVICE = "e49a25f8-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||
UART_WRITE = "e49a25e0-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||
UART_NOTIFY = "e49a28e1-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||
DEFAULT_NAME = "Smart Guitar MIDI"
|
||||
DEFAULT_MTU_PAYLOAD = 20
|
||||
|
||||
|
||||
def ble_midi_encode_sysex(frame: bytes, max_payload: int):
|
||||
ts_hi, ts_lo = 0x80, 0x80
|
||||
pkts = []
|
||||
cap = max_payload - 2
|
||||
first = frame[:cap]
|
||||
pkts.append(bytes([ts_hi, ts_lo]) + first)
|
||||
rest = frame[len(first):]
|
||||
while len(rest) > max_payload - 2:
|
||||
pkts.append(bytes([ts_hi]) + rest[: max_payload - 1])
|
||||
rest = rest[max_payload - 1 :]
|
||||
if rest:
|
||||
pkts.append(bytes([ts_hi, ts_lo]) + rest)
|
||||
return pkts
|
||||
|
||||
|
||||
def ble_midi_decode_packet(payload: bytes) -> bytes:
|
||||
"""Strip BLE-MIDI header/timestamp bytes; keep App SysEx (F0…F7).
|
||||
|
||||
- Framed: [header ts][optional ts][F0 … ts … F7]
|
||||
- Raw (some ATS2853 notifies): [F0 … F7] — must not treat F0 as header
|
||||
(F0/F7 also have bit7=1).
|
||||
"""
|
||||
if not payload:
|
||||
return b""
|
||||
i = 0
|
||||
if payload[0] not in (0xF0, 0xF7) and (payload[0] & 0x80):
|
||||
i = 1 # BLE-MIDI header
|
||||
out = bytearray()
|
||||
while i < len(payload):
|
||||
b = payload[i]
|
||||
i += 1
|
||||
if b in (0xF0, 0xF7):
|
||||
out.append(b)
|
||||
elif b & 0x80:
|
||||
continue # timestamp inside/around SysEx
|
||||
else:
|
||||
out.append(b)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
class BleMidiSim:
|
||||
"""与 AppSim 同接口: send / read_frame / drain / query / close"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name=DEFAULT_NAME,
|
||||
address=None,
|
||||
scan_timeout=20.0,
|
||||
transport="midi",
|
||||
unpair=False,
|
||||
):
|
||||
from bleak import BleakClient, BleakScanner
|
||||
|
||||
self._BleakClient = BleakClient
|
||||
self._BleakScanner = BleakScanner
|
||||
self.parser = FrameParser()
|
||||
self.pending = []
|
||||
self._rx = queue.Queue()
|
||||
self._client = None
|
||||
self._write_char = None
|
||||
self._notify_chars = []
|
||||
self._transport = transport
|
||||
self._unpair = unpair
|
||||
self._loop = asyncio.new_event_loop()
|
||||
self._thread = threading.Thread(target=self._loop.run_forever, daemon=True)
|
||||
self._thread.start()
|
||||
self.info = self._run(self._connect(name, address, scan_timeout))
|
||||
|
||||
def _run(self, coro, timeout=90.0):
|
||||
return asyncio.run_coroutine_threadsafe(coro, self._loop).result(timeout)
|
||||
|
||||
async def _find(self, name, address, scan_timeout):
|
||||
if address:
|
||||
return await self._BleakScanner.find_device_by_address(
|
||||
address, timeout=scan_timeout
|
||||
)
|
||||
print(f"扫描 BLE 设备 ({scan_timeout:.0f}s) ...")
|
||||
# name filter is more reliable than service UUID filter on Windows
|
||||
t0 = time.monotonic()
|
||||
while time.monotonic() - t0 < scan_timeout:
|
||||
rem = max(1.0, scan_timeout - (time.monotonic() - t0))
|
||||
d = await self._BleakScanner.find_device_by_filter(
|
||||
lambda d, a: d.name and name.lower() in d.name.lower(),
|
||||
timeout=min(8.0, rem),
|
||||
)
|
||||
if d:
|
||||
print(f" 发现: {d.name!r} {d.address}")
|
||||
return d
|
||||
print(" ...")
|
||||
return None
|
||||
|
||||
async def _connect(self, name, address, scan_timeout):
|
||||
dev = await self._find(name, address, scan_timeout)
|
||||
if dev is None:
|
||||
raise RuntimeError(f"未找到设备 {name!r}")
|
||||
|
||||
if self._unpair:
|
||||
try:
|
||||
tmp = self._BleakClient(dev.address, timeout=20)
|
||||
await tmp.connect()
|
||||
try:
|
||||
await tmp.unpair()
|
||||
print("已 unpair Windows 残留配对")
|
||||
except Exception as e:
|
||||
print(f"unpair: {e}")
|
||||
try:
|
||||
await tmp.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(1.5)
|
||||
dev = await self._find(name, address, scan_timeout) or dev
|
||||
except Exception as e:
|
||||
print(f"unpair session: {e}")
|
||||
|
||||
client = self._BleakClient(dev, timeout=30)
|
||||
await client.connect()
|
||||
if not client.is_connected:
|
||||
raise RuntimeError("BLE connect 后立即断开")
|
||||
|
||||
write_char = MIDI_CHAR if self._transport == "midi" else UART_WRITE
|
||||
notify_list = (
|
||||
[MIDI_CHAR]
|
||||
if self._transport == "midi"
|
||||
else [UART_NOTIFY, MIDI_CHAR]
|
||||
)
|
||||
|
||||
max_payload = DEFAULT_MTU_PAYLOAD
|
||||
for svc in client.services:
|
||||
for c in svc.characteristics:
|
||||
if c.uuid.lower() == write_char.lower():
|
||||
try:
|
||||
max_payload = c.max_write_without_response_size or max_payload
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_notify(_sender, data):
|
||||
self._rx.put(bytes(data))
|
||||
|
||||
for u in notify_list:
|
||||
try:
|
||||
await client.start_notify(u, _on_notify)
|
||||
self._notify_chars.append(u)
|
||||
except Exception as e:
|
||||
print(f"notify fail {u[:8]}: {e}")
|
||||
|
||||
if not self._notify_chars:
|
||||
raise RuntimeError("无法开启任何 notify(常见原因: Windows 残留配对)")
|
||||
|
||||
self._client = client
|
||||
self._write_char = write_char
|
||||
self._max_payload = max(DEFAULT_MTU_PAYLOAD, max_payload or DEFAULT_MTU_PAYLOAD)
|
||||
return {
|
||||
"name": getattr(dev, "name", None),
|
||||
"address": getattr(dev, "address", address),
|
||||
"max_payload": self._max_payload,
|
||||
"transport": self._transport,
|
||||
"notify": list(self._notify_chars),
|
||||
}
|
||||
|
||||
async def _disconnect(self):
|
||||
if self._client is None:
|
||||
return
|
||||
for u in self._notify_chars:
|
||||
try:
|
||||
await self._client.stop_notify(u)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
await self._client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self._run(self._disconnect(), timeout=10)
|
||||
except Exception:
|
||||
pass
|
||||
self._loop.call_soon_threadsafe(self._loop.stop)
|
||||
self._thread.join(timeout=5)
|
||||
|
||||
def _pump(self):
|
||||
while True:
|
||||
try:
|
||||
data = self._rx.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
if self._transport == "midi":
|
||||
midi = ble_midi_decode_packet(data)
|
||||
else:
|
||||
# uart notify may be raw SysEx, or occasionally BLE-MIDI wrapped
|
||||
midi = (
|
||||
ble_midi_decode_packet(data)
|
||||
if data and (data[0] & 0x80)
|
||||
else data
|
||||
)
|
||||
self.pending.extend(self.parser.feed(midi))
|
||||
|
||||
def send(self, frame: bytes):
|
||||
if self._transport == "midi":
|
||||
pkts = ble_midi_encode_sysex(frame, self._max_payload)
|
||||
else:
|
||||
pkts = [frame]
|
||||
for pkt in pkts:
|
||||
self._run(
|
||||
self._client.write_gatt_char(self._write_char, pkt, response=False),
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
def read_frame(self, timeout=1.0, want_dev=False):
|
||||
deadline = time.monotonic() + timeout
|
||||
while True:
|
||||
self._pump()
|
||||
for i, f in enumerate(self.pending):
|
||||
is_dev = len(f) > 1 and f[1] == DEV_ID
|
||||
if is_dev == want_dev:
|
||||
return self.pending.pop(i)
|
||||
self.pending.clear()
|
||||
if time.monotonic() >= deadline:
|
||||
return None
|
||||
time.sleep(0.01)
|
||||
|
||||
def drain(self, quiet=0.2):
|
||||
while self.read_frame(timeout=quiet) is not None:
|
||||
pass
|
||||
|
||||
def query(self, frame: bytes, timeout=3.0, retry=True):
|
||||
self.drain(quiet=0.15)
|
||||
self.send(frame)
|
||||
r = self.read_frame(timeout=timeout)
|
||||
if r is None and retry:
|
||||
# BLE 偶发丢 Notify:短间隔重发一次(关机指令不可重试)
|
||||
time.sleep(0.25)
|
||||
self.send(frame)
|
||||
r = self.read_frame(timeout=timeout)
|
||||
return r
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
c = self._client
|
||||
try:
|
||||
return bool(c is not None and c.is_connected)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def _reconnect_clean(self, name, address, scan_timeout):
|
||||
"""Drop session, unpair, rescan, reconnect — recovers Windows stale pairing."""
|
||||
await self._disconnect()
|
||||
self._notify_chars = []
|
||||
self._client = None
|
||||
await asyncio.sleep(0.8)
|
||||
self._unpair = True
|
||||
return await self._connect(name, address, scan_timeout)
|
||||
|
||||
def probe_or_rebind(self, name=DEFAULT_NAME, address=None, scan_timeout=20.0):
|
||||
"""Send 01 01; on timeout, unpair+reconnect once and retry."""
|
||||
r = self.query(build(0x01, 0x01), timeout=3.0)
|
||||
if r is not None and len(r) >= 5 and r[0] == HEAD and r[1] == 0x60:
|
||||
return True
|
||||
print("首包无应答 → 自动 unpair 并重连 …")
|
||||
try:
|
||||
self.info = self._run(
|
||||
self._reconnect_clean(name, address, scan_timeout), timeout=120.0
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"重连失败: {e}")
|
||||
return False
|
||||
r = self.query(build(0x01, 0x01), timeout=3.0)
|
||||
ok = r is not None and len(r) >= 5 and r[0] == HEAD and r[1] == 0x60
|
||||
if not ok:
|
||||
print(
|
||||
"仍无应答。请 JLink/断电复位吉他(E49A 裸写或异常断连后模组常停广播),"
|
||||
"并确认琴已开机且蓝牙开关为开。"
|
||||
)
|
||||
return ok
|
||||
|
||||
|
||||
def write_report(R: Results, info: dict, path: str, notes: list[str] | None = None):
|
||||
n = {
|
||||
s: sum(1 for r in R.rows if r[1] == s)
|
||||
for s in ("PASS", "FAIL", "SKIP", "SENT")
|
||||
}
|
||||
lines = [
|
||||
"# BLE SysEx 协议测试报告",
|
||||
"",
|
||||
f"- 日期: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}",
|
||||
f"- 设备: {info.get('name')} ({info.get('address')})",
|
||||
f"- 传输: {info.get('transport')} / bleak, payload={info.get('max_payload')}",
|
||||
f"- notify: {info.get('notify')}",
|
||||
"- 协议来源: Doc/指令测试.docx",
|
||||
"- 测试脚本: tools/test_protocol_ble.py",
|
||||
"",
|
||||
"## 汇总",
|
||||
"",
|
||||
"| PASS | FAIL | SKIP | SENT |",
|
||||
"|------|------|------|------|",
|
||||
f"| {n['PASS']} | {n['FAIL']} | {n['SKIP']} | {n['SENT']} |",
|
||||
"",
|
||||
"## 明细",
|
||||
"",
|
||||
"| # | 用例 | 结果 | 详情 |",
|
||||
"|---|------|------|------|",
|
||||
]
|
||||
for i, (name, status, detail) in enumerate(R.rows, 1):
|
||||
lines.append(f"| {i} | {name} | {status} | {detail} |")
|
||||
lines += ["", "## 备注", ""]
|
||||
for note in notes or []:
|
||||
lines.append(f"- {note}")
|
||||
lines += [
|
||||
"- `01 03`=`Version[]`;`01 04/05`=日期;`01 0C`=`branch_short6`(如 develop_0aedb4);`01 0F`=`BRS08L`。",
|
||||
"- `01 07` UID 为 24 字符 hex ASCII(96-bit),保证 SysEx 7-bit 安全。",
|
||||
"- `04 xx`/`06 xx` 无应答,SENT 表示已发送。",
|
||||
"- `FD 01` 升级指令不在本协议范围。",
|
||||
]
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
return path
|
||||
|
||||
|
||||
def smoke(sim: BleMidiSim) -> Results:
|
||||
R = Results()
|
||||
r = sim.query(build(0x01, 0x01), timeout=3.0)
|
||||
ok = r is not None and len(r) >= 5 and r[0] == HEAD and r[1] == 0x60
|
||||
R.add("smoke 01 01 连接设备", "PASS" if ok else "FAIL", hexs(r))
|
||||
if ok:
|
||||
r2 = sim.query(build(0x01, 0x02), timeout=3.0)
|
||||
ok2 = r2 is not None and r2[2:4] == bytes([0x01, 0x02])
|
||||
R.add("smoke 01 02 设备名", "PASS" if ok2 else "FAIL", hexs(r2))
|
||||
r3 = sim.query(build(0x02, 0x01), timeout=3.0)
|
||||
ok3 = r3 is not None and len(r3) == 47
|
||||
R.add("smoke 02 01 和弦表(长帧)", "PASS" if ok3 else "FAIL", hexs(r3))
|
||||
return R
|
||||
|
||||
|
||||
def selftest():
|
||||
R = Results()
|
||||
pkts = ble_midi_encode_sysex(bytes.fromhex("F0600101F7"), 20)
|
||||
ok = pkts == [bytes.fromhex("8080F0600101F7")]
|
||||
R.add("encode short", "PASS" if ok else "FAIL", str([hexs(p) for p in pkts]))
|
||||
frame = bytes([0xF0, 0x60, 0x02, 0x01]) + bytes(range(1, 43)) + bytes([0xF7])
|
||||
pkts = ble_midi_encode_sysex(frame, 20)
|
||||
back = b"".join(ble_midi_decode_packet(p) for p in pkts)
|
||||
R.add(
|
||||
"encode/decode 47B",
|
||||
"PASS" if back == frame and len(pkts) >= 3 else "FAIL",
|
||||
f"{len(pkts)} pkts",
|
||||
)
|
||||
return R.summary()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="BLE 吉他协议全量测试")
|
||||
ap.add_argument("--ble-name", default=DEFAULT_NAME)
|
||||
ap.add_argument("--ble-address")
|
||||
ap.add_argument(
|
||||
"--transport",
|
||||
choices=("midi", "uart"),
|
||||
default="midi",
|
||||
help="midi=标准 BLE-MIDI 帧; uart=自定义 e49a 原始 SysEx(裸写易弄挂模组)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--unpair",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=(sys.platform == "win32"),
|
||||
help="连接前 unpair(Windows 默认开;--no-unpair 可关)",
|
||||
)
|
||||
ap.add_argument("--allow-poweroff", action="store_true")
|
||||
ap.add_argument("--smoke", action="store_true", help="仅冒烟: 01 01/02 + 02 01")
|
||||
ap.add_argument("--selftest", action="store_true")
|
||||
ap.add_argument("--report")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.selftest:
|
||||
sys.exit(selftest())
|
||||
try:
|
||||
import bleak # noqa: F401
|
||||
except ImportError:
|
||||
sys.exit("缺少 bleak: pip install bleak")
|
||||
|
||||
print(f"选项: unpair={args.unpair} transport={args.transport}")
|
||||
sim = BleMidiSim(
|
||||
name=args.ble_name,
|
||||
address=args.ble_address,
|
||||
transport=args.transport,
|
||||
unpair=args.unpair,
|
||||
)
|
||||
print(
|
||||
f"=== BLE 测试: {sim.info.get('name')} ({sim.info.get('address')}) "
|
||||
f"transport={sim.info.get('transport')} payload={sim.info.get('max_payload')} ==="
|
||||
)
|
||||
if not sim.probe_or_rebind(args.ble_name, args.ble_address):
|
||||
sim.close()
|
||||
path = args.report or os.path.normpath(
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"Doc",
|
||||
"reports",
|
||||
f"ble_sysex_{datetime.datetime.now():%Y%m%d_%H%M%S}.md",
|
||||
)
|
||||
)
|
||||
R = Results()
|
||||
R.add("probe 01 01", "FAIL", "timeout after unpair/reconnect")
|
||||
write_report(
|
||||
R,
|
||||
sim.info or {},
|
||||
path,
|
||||
notes=[
|
||||
"首包无应答:多半是 Windows 残留配对,或 ATS2853 被 E49A 裸写弄挂需复位。",
|
||||
"推荐: python tools/test_protocol_ble.py --unpair",
|
||||
"仍失败则 JLink reset / 给吉他断电再开机后再测。",
|
||||
],
|
||||
)
|
||||
print(f"\n报告已写入: {path}")
|
||||
sys.exit(1)
|
||||
|
||||
notes = [
|
||||
f"transport={sim.info.get('transport')}",
|
||||
f"unpair={args.unpair}",
|
||||
"Windows 默认 --unpair;全 timeout 时先复位吉他再测。",
|
||||
"勿用 --transport uart 裸写做冒烟(易停广播)。",
|
||||
]
|
||||
try:
|
||||
R = smoke(sim) if args.smoke else run_tests(sim, allow_poweroff=args.allow_poweroff)
|
||||
finally:
|
||||
sim.close()
|
||||
code = R.summary()
|
||||
|
||||
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
path = args.report or os.path.normpath(
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"Doc",
|
||||
"reports",
|
||||
f"ble_sysex_{ts}.md",
|
||||
)
|
||||
)
|
||||
write_report(R, sim.info, path, notes=notes)
|
||||
print(f"\n报告已写入: {path}")
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""音师自助:把投放目录的 1/2/3.bin 打成 extflash_ALL_*.res,供 SoundWalkerIAP USB 烧录。
|
||||
|
||||
不走 J-Link;不改固件头文件;不重编 MCU。
|
||||
需要本机已有 ui0902_res.bin(或最近一次发布包里的 ALL.res 可拆出 UI 段)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import struct
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1] # firmware project
|
||||
REPO = ROOT.parent.parent # 一诺国际吉他
|
||||
OUT_DIR = ROOT / "tools" / "out"
|
||||
DROP_DIR = REPO / "Doc" / "音色文件" / "音师投放"
|
||||
IAP_DIR = REPO / "升级" / "MCU主控升级"
|
||||
UI0902_BIN = OUT_DIR / "ui0902_res.bin"
|
||||
REPO_OUT = REPO / "tools" / "out"
|
||||
|
||||
OFF_LOGO = 0x00000000
|
||||
OFF_CHARGING = 0x0000CB70
|
||||
OFF_BIN1 = 0x0001B8F0
|
||||
OFF_BIN2 = 0x0009D07D
|
||||
OFF_BIN3 = 0x000A71AC
|
||||
UI0902_RES_BASE = 0x00100000
|
||||
DAB_MAGIC = b"\xABDAB"
|
||||
MAX_SONG_BIN_BYTES = 41 * 1024
|
||||
BIN2_SLOT_BYTES = OFF_BIN3 - OFF_BIN2
|
||||
|
||||
|
||||
def find_ziliao() -> Path:
|
||||
for p in REPO.iterdir():
|
||||
if p.is_dir() and (p / "logo.bin").is_file() and (p / "Charg.bin").is_file():
|
||||
return p
|
||||
raise SystemExit("找不到 资料/logo.bin 与 Charg.bin,无法打包。")
|
||||
|
||||
|
||||
def dab_ok(blob: bytes, off: int, expect_cnt: int, label: str) -> None:
|
||||
if off + 16 > len(blob):
|
||||
raise SystemExit(f"{label} @0x{off:X}: 文件太短,无法校验")
|
||||
magic = blob[off : off + 4]
|
||||
cnt = struct.unpack_from("<I", blob, off + 12)[0]
|
||||
if magic != DAB_MAGIC:
|
||||
raise SystemExit(
|
||||
f"{label} @0x{off:X}: DAB 魔数错误 {magic.hex()}(期望 {DAB_MAGIC.hex()}),请确认文件是否正确导出"
|
||||
)
|
||||
if cnt != expect_cnt:
|
||||
raise SystemExit(
|
||||
f"{label} @0x{off:X}: 预设数量={cnt},期望 {expect_cnt}。"
|
||||
f"{'若曲目数量有变,需找开发同步固件曲名表。' if label.startswith('2.bin') else ''}"
|
||||
)
|
||||
|
||||
|
||||
def require_equals(blob: bytes, off: int, src: bytes, label: str) -> None:
|
||||
end = off + len(src)
|
||||
if end > len(blob) or blob[off:end] != src:
|
||||
raise SystemExit(f"内部校验失败:{label} 未正确写入资源包")
|
||||
|
||||
|
||||
def find_ui_baseline() -> bytes:
|
||||
if UI0902_BIN.is_file():
|
||||
data = UI0902_BIN.read_bytes()
|
||||
print(f"UI 底图: {UI0902_BIN} ({len(data)} bytes)")
|
||||
return data
|
||||
|
||||
# Fallback: newest release ALL.res under repo tools/out
|
||||
candidates: list[Path] = []
|
||||
if REPO_OUT.is_dir():
|
||||
candidates.extend(REPO_OUT.glob("**/extflash_ALL_*.res"))
|
||||
candidates.extend(REPO_OUT.glob("extflash_ALL_*.res"))
|
||||
candidates = sorted({p.resolve() for p in candidates if p.is_file()}, key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
for all_path in candidates:
|
||||
raw = all_path.read_bytes()
|
||||
if len(raw) > UI0902_RES_BASE:
|
||||
ui = raw[UI0902_RES_BASE:]
|
||||
print(f"UI 底图: 从发布包拆出 {all_path.name} @0x{UI0902_RES_BASE:X} ({len(ui)} bytes)")
|
||||
return ui
|
||||
|
||||
raise SystemExit(
|
||||
"缺少 UI 底图 ui0902_res.bin。\n"
|
||||
"请向开发索取一次含 UI 的基线(tools/out/ui0902_res.bin,或完整 extflash_ALL_*.res),"
|
||||
"放到工程 tools/out/ 后再运行本工具。"
|
||||
)
|
||||
|
||||
|
||||
def pack_tone(bin1: bytes, bin2: bytes, bin3: bytes, logo: bytes, charg: bytes) -> bytes:
|
||||
if len(bin2) > MAX_SONG_BIN_BYTES:
|
||||
raise SystemExit(f"2.bin 过大: {len(bin2)} > {MAX_SONG_BIN_BYTES} (41KB 上限)")
|
||||
if len(bin2) > BIN2_SLOT_BYTES:
|
||||
raise SystemExit(
|
||||
f"2.bin 过大: {len(bin2)} > 槽位 {BIN2_SLOT_BYTES},会挤占 3.bin 固定地址 0x{OFF_BIN3:X}"
|
||||
)
|
||||
|
||||
parts = [
|
||||
(OFF_LOGO, logo, "logo"),
|
||||
(OFF_CHARGING, charg, "Charg"),
|
||||
(OFF_BIN1, bin1, "1.bin"),
|
||||
(OFF_BIN2, bin2, "2.bin"),
|
||||
(OFF_BIN3, bin3, "3.bin"),
|
||||
]
|
||||
blobs: list[bytes] = []
|
||||
cursor = 0
|
||||
for force_off, data, name in parts:
|
||||
if cursor > force_off:
|
||||
raise SystemExit(f"{name}: 前一段过大,无法放到 0x{force_off:X}")
|
||||
if cursor < force_off:
|
||||
blobs.append(b"\xFF" * (force_off - cursor))
|
||||
cursor = force_off
|
||||
blobs.append(data)
|
||||
cursor += len(data)
|
||||
|
||||
packed = b"".join(blobs)
|
||||
dab_ok(packed, OFF_BIN1, 31, "1.bin(节奏)")
|
||||
dab_ok(packed, OFF_BIN2, 1, "2.bin(本地曲目)")
|
||||
dab_ok(packed, OFF_BIN3, 3, "3.bin(万能)")
|
||||
require_equals(packed, OFF_BIN1, bin1, "1.bin")
|
||||
require_equals(packed, OFF_BIN2, bin2, "2.bin")
|
||||
require_equals(packed, OFF_BIN3, bin3, "3.bin")
|
||||
if cursor > UI0902_RES_BASE:
|
||||
raise SystemExit("音色区溢出到 UI 区 (0x100000)")
|
||||
return packed
|
||||
|
||||
|
||||
def write_flash_guide(path: Path, all_name: str, stamp: str) -> None:
|
||||
text = "\n".join(
|
||||
[
|
||||
"音师音色自助包 — 请这样用 SoundWalkerIAP 刷机",
|
||||
f"生成时间: {stamp}",
|
||||
"",
|
||||
"==== 本次只需刷外部 Flash(不用重刷 Boot/APP,除非开发另有说明) ====",
|
||||
f" 文件: {all_name}",
|
||||
" 地址: 0x00000000(从外部 Flash 开头整包写入)",
|
||||
"",
|
||||
"步骤:",
|
||||
" 1. 吉他进入 USB 升级模式(参见 升级步骤.docx)",
|
||||
" 2. 打开本目录的 SoundWalkerIAP.exe",
|
||||
" 3. 先整片擦除外部 Flash",
|
||||
f" 4. 选择 {all_name},烧录到外部 Flash @ 0x00000000",
|
||||
" 5. 退出升级模式,重启,试听节奏/本地曲/万能",
|
||||
"",
|
||||
"注意:",
|
||||
" - 必须刷本包 ALL.res,不要只刷单独的 1/2/3.bin",
|
||||
" - 只刷音色、不带 UI 会导致模式选择页花屏",
|
||||
" - 2.bin 必须 ≤41KB;3.bin 地址固定,不可随 2.bin 变长漂移",
|
||||
" - 若增删本地曲目或改曲名,需找开发同步固件后再测",
|
||||
"",
|
||||
]
|
||||
)
|
||||
path.write_text(text, encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="音师自助打包 1/2/3.bin → ALL.res(USB IAP)")
|
||||
ap.add_argument(
|
||||
"--drop",
|
||||
type=Path,
|
||||
default=DROP_DIR,
|
||||
help=f"投放目录(默认 {DROP_DIR})",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--open",
|
||||
action="store_true",
|
||||
help="完成后用资源管理器打开输出目录",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
drop: Path = args.drop
|
||||
|
||||
print("=== 音师音色打包(USB / SoundWalkerIAP,非 J-Link)===")
|
||||
print(f"投放目录: {drop}")
|
||||
|
||||
bin1_p = drop / "1.bin"
|
||||
bin2_p = drop / "2.bin"
|
||||
bin3_p = drop / "3.bin"
|
||||
missing = [p.name for p in (bin1_p, bin2_p, bin3_p) if not p.is_file()]
|
||||
if missing:
|
||||
raise SystemExit(
|
||||
f"投放目录缺少: {', '.join(missing)}\n"
|
||||
f"请把新的 1.bin / 2.bin / 3.bin 放到:\n {drop}"
|
||||
)
|
||||
|
||||
ziliao = find_ziliao()
|
||||
bin1 = bin1_p.read_bytes()
|
||||
bin2 = bin2_p.read_bytes()
|
||||
bin3 = bin3_p.read_bytes()
|
||||
logo = (ziliao / "logo.bin").read_bytes()
|
||||
charg = (ziliao / "Charg.bin").read_bytes()
|
||||
print(f"1.bin {len(bin1)} bytes | 2.bin {len(bin2)} bytes | 3.bin {len(bin3)} bytes")
|
||||
|
||||
tone = pack_tone(bin1, bin2, bin3, logo, charg)
|
||||
ui = find_ui_baseline()
|
||||
all_res = tone + (b"\xFF" * (UI0902_RES_BASE - len(tone))) + ui
|
||||
require_equals(all_res, OFF_BIN1, bin1, "ALL/1.bin")
|
||||
require_equals(all_res, OFF_BIN2, bin2, "ALL/2.bin")
|
||||
require_equals(all_res, OFF_BIN3, bin3, "ALL/3.bin")
|
||||
dab_ok(all_res, OFF_BIN1, 31, "ALL/1.bin")
|
||||
dab_ok(all_res, OFF_BIN2, 1, "ALL/2.bin")
|
||||
dab_ok(all_res, OFF_BIN3, 3, "ALL/3.bin")
|
||||
|
||||
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
day = datetime.now().strftime("%Y%m%d")
|
||||
pkg = OUT_DIR / f"音师音色包_{day}"
|
||||
pkg.mkdir(parents=True, exist_ok=True)
|
||||
all_name = f"extflash_ALL_artist_{stamp}.res"
|
||||
all_path = pkg / all_name
|
||||
all_path.write_bytes(all_res)
|
||||
(OUT_DIR / "extflash_tone_artist.res").write_bytes(tone)
|
||||
(OUT_DIR / "extflash_ALL_artist.res").write_bytes(all_res)
|
||||
|
||||
iap_exe = IAP_DIR / "SoundWalkerIAP.exe"
|
||||
iap_doc = IAP_DIR / "升级步骤.docx"
|
||||
if not iap_exe.is_file():
|
||||
raise SystemExit(f"找不到 {iap_exe}")
|
||||
shutil.copy2(iap_exe, pkg / "SoundWalkerIAP.exe")
|
||||
if iap_doc.is_file():
|
||||
shutil.copy2(iap_doc, pkg / "升级步骤.docx")
|
||||
write_flash_guide(pkg / "请这样刷.txt", all_name, stamp)
|
||||
|
||||
print(f"OK 音色区 {len(tone)} bytes,ALL.res {len(all_res)} bytes")
|
||||
print(f"OK 输出目录: {pkg}")
|
||||
print(f" - {all_name}")
|
||||
print(" - SoundWalkerIAP.exe")
|
||||
print(" - 请这样刷.txt")
|
||||
print("请用 USB + SoundWalkerIAP 刷 ALL.res,不要用 J-Link 单独烧 bin。")
|
||||
|
||||
if args.open:
|
||||
try:
|
||||
import os
|
||||
|
||||
os.startfile(str(pkg)) # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
print(f"(无法自动打开目录: {exc})")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except SystemExit as e:
|
||||
code = e.code
|
||||
if isinstance(code, str):
|
||||
print(f"\n失败: {code}")
|
||||
sys.exit(1)
|
||||
if code not in (0, None):
|
||||
sys.exit(int(code) if isinstance(code, int) else 1)
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\n失败: {e}")
|
||||
sys.exit(1)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
音师投放与自助更新说明
|
||||
========================
|
||||
|
||||
一、投放目录(放新的 1/2/3.bin)
|
||||
一诺国际吉他\Doc\音色文件\音师投放\
|
||||
1.bin 普通/专业节奏
|
||||
2.bin 本地曲目(必须 ≤41KB)
|
||||
3.bin 万能模式
|
||||
|
||||
二、一键打包(合并 ALL.res,USB 烧录,不用 J-Link)
|
||||
双击:本目录「音师更新音色.bat」
|
||||
或命令行:python tone_artist_pack.py --open
|
||||
|
||||
三、烧录
|
||||
输出在 tools\out\音师音色包_日期\
|
||||
- extflash_ALL_artist_*.res
|
||||
- SoundWalkerIAP.exe
|
||||
- 请这样刷.txt
|
||||
按「请这样刷.txt」:进 USB 升级模式 → 擦除外部 Flash
|
||||
→ SoundWalkerIAP 将 ALL.res 烧到 0x00000000 → 重启试听
|
||||
|
||||
四、限制
|
||||
- 只更新音色时一般不用重刷 MCU
|
||||
- 必须刷 ALL.res(含界面),勿只刷单独 bin,否则模式选择花屏
|
||||
- 增删本地曲/改曲名需找开发同步固件曲名表
|
||||
- 首次需本机有 tools\out\ui0902_res.bin(向开发索取一次基线即可)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo 音师音色更新 - 打包 ALL.res (USB IAP)
|
||||
echo 不使用 J-Link,请用 SoundWalkerIAP 烧录
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
where python >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [错误] 未找到 python,请先安装 Python 3 并勾选 Add to PATH。
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
python "%~dp0tone_artist_pack.py" --open
|
||||
set ERR=%ERRORLEVEL%
|
||||
echo.
|
||||
if not "%ERR%"=="0" (
|
||||
echo 打包失败,请根据上方中文提示检查 1/2/3.bin。
|
||||
pause
|
||||
exit /b %ERR%
|
||||
)
|
||||
|
||||
echo 打包成功。请按输出目录中的「请这样刷.txt」用 SoundWalkerIAP 刷机。
|
||||
pause
|
||||
exit /b 0
|
||||