diff --git a/Global/Global.c b/Global/Global.c index 0d8856b..ad88ad2 100644 --- a/Global/Global.c +++ b/Global/Global.c @@ -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,4}; /* major.minor.patch — 0.2.4: Boot shows UI0902 flash-mode screen */ +const uint8_t Version[3] = {0,2,4}; /* major.minor.patch — 0.2.4: Boot font 「烧录模式」+ release with Boot */ uint8_t Led = 8; bool PressFlag = 0; @@ -259,6 +259,23 @@ static void PowerOff(void) } } +/* 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(); diff --git a/Global/Global.h b/Global/Global.h index 1087ac1..66e9c75 100644 --- a/Global/Global.h +++ b/Global/Global.h @@ -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; diff --git a/project/src/main.c b/project/src/main.c index 2e7d6b2..783bc9d 100644 --- a/project/src/main.c +++ b/project/src/main.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(); diff --git a/protocol/bl_uart_parse.c b/protocol/bl_uart_parse.c index 370e7df..473c46a 100644 --- a/protocol/bl_uart_parse.c +++ b/protocol/bl_uart_parse.c @@ -25,7 +25,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����ȫ�ֱ��� static UART4_RCV_StatusType UART4_RCV_Status = UART4_RCV_BUFF_IDLE; @@ -48,22 +48,25 @@ typedef struct // ============================== // ָ�������������ȫ�� static�� // ============================== -//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 +78,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 +100,53 @@ static void handleSectionD(uint8_t *data); *************************************************/ static const BLE_SysExCmdItem bleSysExCmdTable[] = { -// //======== 1. ��ȡ������Ϣ 0x01 ======== -// {{0x01, 0x00}, handleDevDisconnect}, // �Ͽ��豸 -// {{0x01, 0x01}, handleDevConnect}, // �����豸 -// {{0x01, 0x02}, handleDevName}, // ��ȡ�豸�� -// {{0x01, 0x03}, handleFwMainVer}, // �̼����汾 -// {{0x01, 0x04}, handleSoundVer}, // ��Դ�汾 -// {{0x01, 0x05}, handleUIVer}, // UI�汾 -// {{0x01, 0x06}, handleOtherInfo}, // ������Ϣ -// {{0x01, 0x07}, handleDevCode}, // �豸���� -// {{0x01, 0x0A}, handleAutoPowerOff}, // �Զ��ػ� -// -// //======== 2. ӳ������ 0x02 ======== - {{0x02, 0x01}, handleReadRhythmMap}, // ��ȡ����/����ӳ��� - {{0x02, 0x02}, handlePitchOffset}, // Pitchƫ�� - {{0x02, 0x03}, handleChordOffset}, // Chordƫ�� -// {{0x02, 0x04}, handleResetChordMap}, // ���ú���ӳ��Ĭ��ֵ + /*======== 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, 0xFF}, handleFwCode}, /* fw code */ - //======== 3. ����������д 0x03 ======== -// {{0x03, 0x04}, handleRhythmStyle}, // ��ȡ/���ý����� - {{0x03, 0x05}, handleStringTimbre}, // ��ȡ/��������ɫ - {{0x03, 0x06}, handleBPM}, // ��ȡ/����BPM - {{0x03, 0x07}, handleTranspose}, // ��ȡ/�����Ƶ� + /*======== 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. ����/LED���� 0x04 ======== - {{0x04, 0x00}, handleLED0}, // ��1��LED - {{0x04, 0x01}, handleLED1}, // ��2��LED - {{0x04, 0x02}, handleLED2}, // ��3��LED - {{0x04, 0x03}, handleLED3}, // ��4��LED - {{0x04, 0x04}, handleLED4}, // ��5��LED - {{0x04, 0x05}, handleLED5}, // ��6��LED - {{0x04, 0x06}, handleLED6}, // ��7��LED -// {{0x04, 0x07}, handleStopPlay}, // �������� + /*======== 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. ��λ/�ػ� 0x05 ======== -// {{0x05, 0x00}, handleDeviceReset}, // �豸��λ -// -// //======== 6. ������ת 0x06 ======== - {{0x06, 0x01}, handleIntro}, // ǰ�� - {{0x06, 0x02}, handleInterlude}, // ���� - {{0x06, 0x03}, handleOutro}, // β�� - {{0x04, 0x07}, handleEnd}, // end - {{0x06, 0x05}, handleSectionA}, // A�� - {{0x06, 0x06}, handleSectionB}, // B�� - {{0x06, 0x07}, handleSectionC}, // C�� - {{0x06, 0x08}, handleSectionD}, // D�� + /*======== 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 */ }; // ָ������ @@ -482,7 +485,7 @@ static void handleChordOffset(uint8_t *data) 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}; + uint8_t ReadChordMap[47] = {0xF0, 0x60, 0x02, 0x01, 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}; uint8_t chord_map = 0; for(uint8_t i = 1;i < 22;i ++) { @@ -546,7 +549,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 +573,195 @@ 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 BLE_DEVICE_NAME "PHON_145" /* must match BLE module broadcast name */ +#define AT32_UID_BASE 0x1FFFF7E8UL /* 96-bit unique device ID */ + +/* placeholders: sound fw lives on Dream DSP side, UI/user-fw ver bumped per release */ +static const uint8_t SOUND_VER[4] = {0x4D, 0x06, 0x7F, 0x01}; +static const uint8_t UI_VER[4] = {0x21, 0x05, 0x7F, 0x01}; +static const uint8_t USER_FW_VER[4] = {0x4D, 0x06, 0x7F, 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) +{ + uint8_t resp[4 + sizeof(BLE_DEVICE_NAME)]; /* head4 + name + tail1 */ + (void)data; + resp[0] = 0xF0; resp[1] = 0x60; resp[2] = 0x01; resp[3] = 0x02; + memcpy(&resp[4], BLE_DEVICE_NAME, sizeof(BLE_DEVICE_NAME) - 1); + resp[sizeof(resp) - 1] = 0xF7; + USART4_SendData(resp, 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) +{ + 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) +{ + uint8_t resp[17] = {0xF0, 0x60, 0x01, 0x07}; + (void)data; + memcpy(&resp[4], (const void *)AT32_UID_BASE, 12); + resp[16] = 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) +{ + uint8_t resp[9] = {0xF0, 0x60, 0x01, 0x0C, USER_FW_VER[0], USER_FW_VER[1], USER_FW_VER[2], USER_FW_VER[3], 0xF7}; + (void)data; + USART4_SendData(resp, sizeof(resp)); +} + +/* F0 60 01 11 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) +{ + uint8_t resp[16]; + uint8_t len = (uint8_t)strlen(fwname); /* "BRS08L" */ + (void)data; + resp[0] = 0xF0; resp[1] = 0x60; resp[2] = 0x01; resp[3] = 0xFF; + memcpy(&resp[4], fwname, len); + resp[4 + len] = 0xF7; + USART4_SendData(resp, (uint16_t)(4 + len + 1)); +} + +/*-------- 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 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 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(); + resp[5] = (uint8_t)(mGuiData[GUI_SPEED].Current & 0x7F); /* doc: single-byte bpm */ + USART4_SendData(resp, sizeof(resp)); + } + break; + case 2: /* user style list page: reply total count */ + case 3: /* user style total */ + { + uint8_t resp[8] = {0xF0, 0x60, 0x03, 0x04, 0x00, 0x00, 0x00, 0xF7}; + resp[4] = data[4]; + resp[5] = (uint8_t)(LOCAL_SONG_COUNT / 128); + resp[6] = (uint8_t)(LOCAL_SONG_COUNT % 128); + 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; + USART4_SendData(resp, sizeof(resp)); + System_RequestPowerOff(); +} diff --git a/task/task_init.c b/task/task_init.c index 6a2c369..a95016a 100644 --- a/task/task_init.c +++ b/task/task_init.c @@ -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; } } } diff --git a/tools/test_protocol_app_sim.py b/tools/test_protocol_app_sim.py new file mode 100644 index 0000000..57fb2b9 --- /dev/null +++ b/tools/test_protocol_app_sim.py @@ -0,0 +1,369 @@ +# -*- coding: utf-8 -*- +""" +test_protocol_app_sim.py — 模拟手机 App 对吉他固件做全协议串口测试 + +协议来源: Doc/指令测试.docx (= 指令测试0907.docx) +通道: UART4 (BLE 桥), 115200 8N1; 帧格式 F0 60 [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 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 + self.buf.append(b) + if b == TAIL or len(self.buf) >= 64: + if b == TAIL: + 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 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 in ((0x03, "固件主版本"), (0x04, "音源版本"), + (0x05, "UI版本"), (0x0C, "用户固件版本")): + r = sim.query(build(0x01, sub)) + ok, d = expect(r, (0x60, 0x01, sub), 9) + R.add(f"01 {sub:02X} {name}", "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), 17) + R.add("01 07 设备编码(UID)", "PASS" if ok else "FAIL", d) + + r = sim.query(build(0x01, 0xFF)) + ok, d = expect(r, (0x60, 0x01, 0xFF)) + R.add("01 FF 固件编码", "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 + + 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) + + 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) + + r = sim.query(build(0x03, 0x04, 0x02, 0x00, 0x00, 0x00, 0x07)) + ok, d = expect(r, (0x60, 0x03, 0x04, 0x02), 8) + R.add("03 04 用户风格分页", "PASS" if ok else "FAIL", d) + + r = sim.query(build(0x03, 0x04, 0x03)) + ok, d = expect(r, (0x60, 0x03, 0x04, 0x03), 8) + 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 + + # 移调: 读 -> 写 -> 读 -> 恢复 + r0 = sim.query(build(0x03, 0x07, 0x00)) + ok, d = expect(r0, (0x60, 0x03, 0x07, 0x00), 8) + 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) == 8 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: + r = sim.query(build(0x05, 0x00), timeout=2.0) + ok, d = expect(r, (0x60, 0x05, 0x00), 6) + R.add("05 00 复位/关机", "PASS" if ok else "FAIL", d + " (设备将软关机)") + else: + R.add("05 00 复位/关机", "SKIP", "需 --allow-poweroff") + + return R.summary() + + +# ---------------------------------------------------------------- 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) + finally: + sim.close() + sys.exit(code) + + +if __name__ == "__main__": + main()