Start BLE UART4 App protocol receive on power-on.
TaskBTRecvThread previously started only after mode select, so GATT SysEx never reached the parser until then; verified BLE-MIDI 01 01 now replies with GATT notify. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f762348883
commit
7b08f5e098
|
|
@ -184,6 +184,7 @@ void System_PowerOn(void)
|
||||||
BSP_MainPowerEnable(1);
|
BSP_MainPowerEnable(1);
|
||||||
BSP_DreamCorePowerEnable(1);
|
BSP_DreamCorePowerEnable(1);
|
||||||
BSP_HT7178PowerEnable(1);
|
BSP_HT7178PowerEnable(1);
|
||||||
|
/* 蓝牙按 NVM/系统设置开关恢复,勿强制常开 */
|
||||||
BSP_BlueToothPowerEnable((uint8_t)(mGuiData[GUI_BL_SW].Current ? 1 : 0));
|
BSP_BlueToothPowerEnable((uint8_t)(mGuiData[GUI_BL_SW].Current ? 1 : 0));
|
||||||
powon = true; /* 更新开机标志 */
|
powon = true; /* 更新开机标志 */
|
||||||
|
|
||||||
|
|
@ -196,7 +197,9 @@ void System_PowerOn(void)
|
||||||
app_tm1617_init();
|
app_tm1617_init();
|
||||||
XPT2046_Init();
|
XPT2046_Init();
|
||||||
App_Auto_Init();
|
App_Auto_Init();
|
||||||
LOG_I("PWR", "periph ready");
|
/* App 协议走 UART4:须在开机后即解析,不能等到触摸选模式才 StartTask */
|
||||||
|
StartTask();
|
||||||
|
LOG_I("PWR", "periph ready + BT UART recv started");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void PowerOn(void)
|
static void PowerOn(void)
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
|
|
||||||
#define BUFF_SIZE 512
|
#define BUFF_SIZE 512
|
||||||
|
|
||||||
/* 纯比较宏:参数必须是普通局部变量(非 volatile),
|
/* ?????????????????????????????? volatile????
|
||||||
避免同一表达式多次访问 volatile 触发 Pa082 且逻辑不稳 */
|
?????????????????? volatile ???? Pa082 ????????? */
|
||||||
#define IS_FULL(head, tail) ((((head) + 1) % BUFF_SIZE) == (tail))
|
#define IS_FULL(head, tail) ((((head) + 1) % BUFF_SIZE) == (tail))
|
||||||
#define IS_EMPTY(head, tail) ((head) == (tail))
|
#define IS_EMPTY(head, tail) ((head) == (tail))
|
||||||
|
|
||||||
/* ==================== 环形队列结构体 ==================== */
|
/* ==================== ???????????? ==================== */
|
||||||
|
|
||||||
/* 一个串口实例的环形队列(收发各一) */
|
/* ????????????????????????????? */
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
uint8_t rx_buff[BUFF_SIZE];
|
uint8_t rx_buff[BUFF_SIZE];
|
||||||
|
|
@ -21,14 +21,14 @@ typedef struct
|
||||||
volatile uint16_t tx_head;
|
volatile uint16_t tx_head;
|
||||||
volatile uint16_t tx_tail;
|
volatile uint16_t tx_tail;
|
||||||
|
|
||||||
usart_type *usart; /* 关联的串口外设(USART2 / UART4) */
|
usart_type *usart; /* ??????????????USART2 / UART4?? */
|
||||||
} uart_ring_t;
|
} uart_ring_t;
|
||||||
|
|
||||||
/* USART2 和 UART4 各一个实例 */
|
/* USART2 ?? UART4 ???????? */
|
||||||
static uart_ring_t uart2;
|
static uart_ring_t uart2;
|
||||||
static uart_ring_t uart4;
|
static uart_ring_t uart4;
|
||||||
|
|
||||||
/* 初始化:关联串口外设(由 wk_usart2_init / wk_uart4_init 调用) */
|
/* ?????????????????????? wk_usart2_init / wk_uart4_init ????? */
|
||||||
void uart_ring_usart2_init(void)
|
void uart_ring_usart2_init(void)
|
||||||
{
|
{
|
||||||
uart2.usart = USART2;
|
uart2.usart = USART2;
|
||||||
|
|
@ -39,19 +39,19 @@ void uart_ring_uart4_init(void)
|
||||||
uart4.usart = UART4;
|
uart4.usart = UART4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 通用入队/出队(带实例参数) ==================== */
|
/* ==================== ??????/???????????????? ==================== */
|
||||||
|
|
||||||
/* 入队(ISR 调用)。先快照 volatile 值再操作,避免 Pa082 */
|
/* ????ISR ???????????? volatile ???????????? Pa082 */
|
||||||
static void ring_rx_push(uart_ring_t *rb, uint8_t c)
|
static void ring_rx_push(uart_ring_t *rb, uint8_t c)
|
||||||
{
|
{
|
||||||
uint16_t h = rb->rx_head;
|
uint16_t h = rb->rx_head;
|
||||||
uint16_t t = rb->rx_tail;
|
uint16_t t = rb->rx_tail;
|
||||||
if(IS_FULL(h, t)) return; /* 满则丢,防止覆盖未读数据 */
|
if(IS_FULL(h, t)) return; /* ???????????????????? */
|
||||||
rb->rx_buff[h] = c;
|
rb->rx_buff[h] = c;
|
||||||
rb->rx_head = (h + 1) % BUFF_SIZE;
|
rb->rx_head = (h + 1) % BUFF_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 出队(业务线程调用) */
|
/* ??????????????? */
|
||||||
static uint8_t ring_rx_pop(uart_ring_t *rb, uint8_t *c)
|
static uint8_t ring_rx_pop(uart_ring_t *rb, uint8_t *c)
|
||||||
{
|
{
|
||||||
uint16_t h = rb->rx_head;
|
uint16_t h = rb->rx_head;
|
||||||
|
|
@ -62,65 +62,64 @@ static uint8_t ring_rx_pop(uart_ring_t *rb, uint8_t *c)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 写入 tx 队列(调用前应已关中断,防止 ISR 同时读) */
|
/* ???? tx ??????????????????????? ISR ?????? */
|
||||||
static void ring_tx_write(uart_ring_t *rb, uint8_t *data, uint16_t len)
|
static void ring_tx_write(uart_ring_t *rb, uint8_t *data, uint16_t len)
|
||||||
{
|
{
|
||||||
uint16_t h = rb->tx_head;
|
uint16_t h = rb->tx_head;
|
||||||
uint16_t t = rb->tx_tail;
|
uint16_t t = rb->tx_tail;
|
||||||
for(uint16_t i = 0; i < len; i++)
|
for(uint16_t i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
if(IS_FULL(h, t)) break; /* 用 tx 判满 */
|
if(IS_FULL(h, t)) break; /* ?? tx ???? */
|
||||||
rb->tx_buff[h] = data[i];
|
rb->tx_buff[h] = data[i];
|
||||||
h = (h + 1) % BUFF_SIZE;
|
h = (h + 1) % BUFF_SIZE;
|
||||||
}
|
}
|
||||||
rb->tx_head = h; /* 最后一次性写回 volatile */
|
rb->tx_head = h; /* ???????????? volatile */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 发送接口(带串口参数) ==================== */
|
/* ==================== ?????????????????? ==================== */
|
||||||
|
|
||||||
/* 通用发送:关中断 → 写队列 → 开 TDBE 中断触发发送 */
|
/* ?????????????? ?? ?????? ?? ?? TDBE ??????????? */
|
||||||
static void uart_ring_send(uart_ring_t *rb, uint8_t *data, uint16_t len)
|
static void uart_ring_send(uart_ring_t *rb, uint8_t *data, uint16_t len)
|
||||||
{
|
{
|
||||||
rt_base_t level = rt_hw_interrupt_disable();
|
rt_base_t level = rt_hw_interrupt_disable();
|
||||||
|
|
||||||
ring_tx_write(rb, data, len);
|
ring_tx_write(rb, data, len);
|
||||||
usart_interrupt_enable(rb->usart, USART_TDBE_INT, TRUE); /* 开发送中断 */
|
usart_interrupt_enable(rb->usart, USART_TDBE_INT, TRUE); /* ?????????? */
|
||||||
|
|
||||||
rt_hw_interrupt_enable(level);
|
rt_hw_interrupt_enable(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* USART2 发送(对外,midi_send.c 等在用) */
|
/* USART2 ?????????midi_send.c ??????? */
|
||||||
void USART2_SendData(uint8_t *data, uint16_t len)
|
void USART2_SendData(uint8_t *data, uint16_t len)
|
||||||
{
|
{
|
||||||
uart_ring_send(&uart2, data, len);
|
uart_ring_send(&uart2, data, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* UART4 发送(对外) */
|
/* UART4 ????????? */
|
||||||
void USART4_SendData(uint8_t *data, uint16_t len)
|
void USART4_SendData(uint8_t *data, uint16_t len)
|
||||||
{
|
{
|
||||||
uart_ring_send(&uart4, data, len);
|
uart_ring_send(&uart4, data, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* USART2 接收出队(对外) */
|
/* USART2 ???????????? */
|
||||||
uint8_t USART2_RxPop(uint8_t *c)
|
uint8_t USART2_RxPop(uint8_t *c)
|
||||||
{
|
{
|
||||||
return ring_rx_pop(&uart2, c);
|
return ring_rx_pop(&uart2, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* UART4 接收出队(对外) */
|
/* UART4 ???????????? */
|
||||||
uint8_t UART4_RxPop(uint8_t *c)
|
uint8_t UART4_RxPop(uint8_t *c)
|
||||||
{
|
{
|
||||||
return ring_rx_pop(&uart4, c);
|
return ring_rx_pop(&uart4, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 串口中断 ==================== */
|
/* ==================== ???????? ==================== */
|
||||||
|
|
||||||
static void uart_ring_isr(uart_ring_t *rb)
|
static void uart_ring_isr(uart_ring_t *rb)
|
||||||
{
|
{
|
||||||
/* 接收:收到字节 → 入 rx 队列 */
|
|
||||||
if(usart_flag_get(rb->usart, USART_RDBF_FLAG) == SET)
|
if(usart_flag_get(rb->usart, USART_RDBF_FLAG) == SET)
|
||||||
{
|
{
|
||||||
uint8_t data = usart_data_receive(rb->usart); /* 读数据(自动清 RDBF) */
|
uint8_t data = usart_data_receive(rb->usart);
|
||||||
ring_rx_push(rb, data);
|
ring_rx_push(rb, data);
|
||||||
if(rb->usart == UART4)
|
if(rb->usart == UART4)
|
||||||
{
|
{
|
||||||
|
|
@ -128,7 +127,6 @@ static void uart_ring_isr(uart_ring_t *rb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 发送:tx 队列有数据 → 发一个字节;发完 → 关 TDBE 中断 */
|
|
||||||
if(usart_flag_get(rb->usart, USART_TDBE_FLAG) == SET)
|
if(usart_flag_get(rb->usart, USART_TDBE_FLAG) == SET)
|
||||||
{
|
{
|
||||||
uint16_t h = rb->tx_head;
|
uint16_t h = rb->tx_head;
|
||||||
|
|
@ -140,11 +138,10 @@ static void uart_ring_isr(uart_ring_t *rb)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
usart_interrupt_enable(rb->usart, USART_TDBE_INT, FALSE); /* 发完关中断 */
|
usart_interrupt_enable(rb->usart, USART_TDBE_INT, FALSE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 错误处理:清溢出标志,防止反复进中断卡死 */
|
|
||||||
if(usart_flag_get(rb->usart, USART_ROERR_FLAG) == SET)
|
if(usart_flag_get(rb->usart, USART_ROERR_FLAG) == SET)
|
||||||
usart_flag_clear(rb->usart, USART_ROERR_FLAG);
|
usart_flag_clear(rb->usart, USART_ROERR_FLAG);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -162,13 +162,14 @@ static const BLE_SysExCmdItem bleSysExCmdTable[] =
|
||||||
void processBLESysEXData(uint8_t* data, uint8_t cnt)
|
void processBLESysEXData(uint8_t* data, uint8_t cnt)
|
||||||
{
|
{
|
||||||
//ResetAutoPowerCount();
|
//ResetAutoPowerCount();
|
||||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>֡У<EFBFBD><EFBFBD>
|
// ֡У<EFBFBD><EFBFBD>
|
||||||
if (data[0] != FRAME_SYS_HEAD || data[cnt - 1] != FRAME_SYS_TAIL || cnt > UART4_PROCESS_BUFF_SIZE || data[1] != 0x60)
|
if (data[0] != FRAME_SYS_HEAD || data[cnt - 1] != FRAME_SYS_TAIL || cnt > UART4_PROCESS_BUFF_SIZE || data[1] != 0x60)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
LOG_I("BLE", "sysex ok len=%u cmd=%02X %02X", (unsigned)cnt, data[2], data[3]);
|
||||||
|
|
||||||
// <EFBFBD><EFBFBD>ȡ <EFBFBD><EFBFBD>ָ<EFBFBD><EFBFBD> + <20><>ָ<EFBFBD><D6B8>
|
// ȡ<EFBFBD><EFBFBD>ָ<EFBFBD><EFBFBD> + <20><>ָ<EFBFBD><D6B8>
|
||||||
uint8_t cmd[2] = {data[2], data[3]};
|
uint8_t cmd[2] = {data[2], data[3]};
|
||||||
|
|
||||||
// <20><><EFBFBD><EFBFBD>ָ<EFBFBD><D6B8><EFBFBD>ƥ<EFBFBD><C6A5>
|
// <20><><EFBFBD><EFBFBD>ָ<EFBFBD><D6B8><EFBFBD>ƥ<EFBFBD><C6A5>
|
||||||
|
|
@ -198,6 +199,7 @@ void UART4_Data_Process(volatile uint8_t* data)
|
||||||
last_tick = now;
|
last_tick = now;
|
||||||
memset(UART4_Process_Buff, 0, sizeof(UART4_Process_Buff));
|
memset(UART4_Process_Buff, 0, sizeof(UART4_Process_Buff));
|
||||||
UART4_RCV_cnt = 0;
|
UART4_RCV_cnt = 0;
|
||||||
|
UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (UART4_RCV_Status)
|
switch (UART4_RCV_Status)
|
||||||
|
|
@ -254,7 +256,6 @@ void UART4_Data_Process(volatile uint8_t* data)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// <20>Ƿ<EFBFBD><C7B7>ֽڣ<D6BD><DAA3><EFBFBD>λ
|
|
||||||
UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
UART4_RCV_Status = UART4_RCV_BUFF_IDLE;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -128,11 +128,10 @@ void TaskBTRecvThread_entry(void* parameter)
|
||||||
uint8_t c;
|
uint8_t c;
|
||||||
while(1)
|
while(1)
|
||||||
{
|
{
|
||||||
if(rt_sem_take(UART4_sem,RT_WAITING_FOREVER) == RT_EOK)
|
if(rt_sem_take(UART4_sem, RT_WAITING_FOREVER) == RT_EOK)
|
||||||
{
|
{
|
||||||
while(UART4_RxPop(&c))
|
while(UART4_RxPop(&c))
|
||||||
{
|
{
|
||||||
//解析函数
|
|
||||||
UART4_Data_Process(&c);
|
UART4_Data_Process(&c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 historically)
|
||||||
|
await ble_write(
|
||||||
|
"midi-framed",
|
||||||
|
MIDI,
|
||||||
|
bytes.fromhex("8080F0600101F7"),
|
||||||
|
[MIDI, UARTN, EFF2],
|
||||||
|
)
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
|
||||||
|
# 2) custom uart raw (may drop link)
|
||||||
|
await ble_write(
|
||||||
|
"uartw-raw",
|
||||||
|
UARTW,
|
||||||
|
bytes.fromhex("F0600101F7"),
|
||||||
|
[UARTN, MIDI, EFF2],
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
stop = True
|
||||||
|
time.sleep(0.4)
|
||||||
|
j.close()
|
||||||
|
|
||||||
|
text = "".join(lines)
|
||||||
|
print("\n===== ANALYSIS =====", flush=True)
|
||||||
|
print("has U4 rx sniff:", ("rx n=" in text) or ("U4" in text and "rx" in text), flush=True)
|
||||||
|
print("has sysex ok:", "sysex ok" in text, flush=True)
|
||||||
|
print("has sysex reject:", "sysex reject" in text, flush=True)
|
||||||
|
print("has sysex abort:", "sysex abort" in text, flush=True)
|
||||||
|
print("has U4 tx:", ("U4" in text and "tx" in text) or "tx len=" in text, flush=True)
|
||||||
|
print("has pin diag:", "edge PC" in text or "pinmap" in text, flush=True)
|
||||||
|
# Extract last diag line if present
|
||||||
|
for line in text.splitlines():
|
||||||
|
if "edge PC10=" in line:
|
||||||
|
print("diag:", line.strip(), flush=True)
|
||||||
|
if "sysex ok" in text and "tx len=" in text:
|
||||||
|
print("VERDICT: MCU got cmd and replied on UART4 → GATT notify path broken in BLE module", flush=True)
|
||||||
|
elif "rx n=" in text and "sysex ok" not in text:
|
||||||
|
print("VERDICT: UART4 got bytes but frame not accepted → protocol/framing", flush=True)
|
||||||
|
elif "edge PC12=" in text and "isr_rx=0" in text:
|
||||||
|
# SCH puts B_UART4_RX on PC12; FW UART4 RX is PC11
|
||||||
|
print("VERDICT: activity on PC12 while UART4 ISR idle → driver pinmap vs SCH (MCU底层)", flush=True)
|
||||||
|
elif "edge PC11=" in text and "isr_rx=0" in text:
|
||||||
|
print("VERDICT: edges on PC11 but no UART ISR → baud/noise or not UART framing", flush=True)
|
||||||
|
elif "isr_rx=0" in text and "edge PC10=0" in text and "edge PC11=0" in text and "edge PC12=0" in text:
|
||||||
|
print("VERDICT: no GPIO edges + no UART ISR → ATS2853 did not drive UART (模组固件/桥接)", flush=True)
|
||||||
|
elif "U4" not in text and "rx" not in text:
|
||||||
|
print("VERDICT: no UART4 activity → BLE module did not forward GATT write to UART4 (or RTT dead)", flush=True)
|
||||||
|
else:
|
||||||
|
print("VERDICT: inconclusive — see RTT dump above", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
|
@ -0,0 +1,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()
|
||||||
|
|
@ -1,25 +1,28 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
test_protocol_ble.py — 通过笔记本蓝牙(BLE-MIDI)对吉他做全协议测试并出报告
|
test_protocol_ble.py — 通过笔记本蓝牙对吉他做全协议测试并出报告
|
||||||
|
|
||||||
协议来源: Doc/指令测试.docx
|
协议来源: Doc/指令测试.docx
|
||||||
链路: 笔记本 BLE <-> "Smart Guitar MIDI" (BLE-MIDI) <-> 吉他 UART4
|
链路: 笔记本 BLE <-> "Smart Guitar MIDI" <-> 吉他 UART4
|
||||||
App->设备: F0 60 ... F7 ; 设备主动上报: F0 51 ... F7
|
App->设备: F0 60 ... F7 ; 设备主动上报: F0 51 ... F7
|
||||||
|
|
||||||
BLE-MIDI:
|
GATT:
|
||||||
service 03B80E5A-EDE8-4B33-A751-6CE34EC4C700
|
BLE-MIDI 03B80E5A-... / 7772E5DB-... (framed SysEx)
|
||||||
characteristic 7772E5DB-3868-4112-A1A9-F2669D106BF3 (notify + write-no-resp)
|
自定义串口 e49a25f8-... / e49a25e0(写) + e49a28e1(通知) (raw SysEx)
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
python test_protocol_ble.py # 扫描并按名连接
|
python test_protocol_ble.py
|
||||||
python test_protocol_ble.py --ble-address AA:BB:CC:... # 按地址连接
|
python test_protocol_ble.py --transport midi
|
||||||
python test_protocol_ble.py --allow-poweroff # 含 05 00 关机用例
|
python test_protocol_ble.py --transport uart
|
||||||
python test_protocol_ble.py --selftest # 无硬件自检(编解码)
|
python test_protocol_ble.py --unpair
|
||||||
|
python test_protocol_ble.py --smoke
|
||||||
|
python test_protocol_ble.py --selftest
|
||||||
|
|
||||||
依赖: bleak (pip install bleak)
|
依赖: bleak (pip install bleak)
|
||||||
报告: Doc/reports/ble_sysex_<时间戳>.md
|
报告: Doc/reports/ble_sysex_<时间戳>.md
|
||||||
退出码: 0 = 全部通过, 1 = 有 FAIL
|
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
|
|
@ -31,118 +34,173 @@ import time
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
from test_protocol_app_sim import ( # noqa: E402
|
from test_protocol_app_sim import ( # noqa: E402
|
||||||
HEAD, DEV_ID, build, FrameParser, Results, expect, run_tests, hexs)
|
HEAD, DEV_ID, build, FrameParser, Results, run_tests, hexs)
|
||||||
|
|
||||||
MIDI_SERVICE = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"
|
MIDI_SERVICE = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"
|
||||||
MIDI_CHAR = "7772E5DB-3868-4112-A1A9-F2669D106BF3"
|
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_NAME = "Smart Guitar MIDI"
|
||||||
DEFAULT_MTU_PAYLOAD = 20 # ATT 默认 23 -> 应用载荷 20
|
DEFAULT_MTU_PAYLOAD = 20
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------- BLE-MIDI codec
|
|
||||||
def ble_midi_encode_sysex(frame: bytes, max_payload: int):
|
def ble_midi_encode_sysex(frame: bytes, max_payload: int):
|
||||||
"""完整 SysEx(F0...F7) -> BLE-MIDI 包列表 (ts=0)"""
|
|
||||||
ts_hi, ts_lo = 0x80, 0x80
|
ts_hi, ts_lo = 0x80, 0x80
|
||||||
pkts = []
|
pkts = []
|
||||||
cap = max_payload - 2 # header + timestamp
|
cap = max_payload - 2
|
||||||
first = frame[:cap]
|
first = frame[:cap]
|
||||||
pkts.append(bytes([ts_hi, ts_lo]) + first)
|
pkts.append(bytes([ts_hi, ts_lo]) + first)
|
||||||
rest = frame[len(first):]
|
rest = frame[len(first):]
|
||||||
while len(rest) > max_payload - 2: # 中间包: header + data
|
while len(rest) > max_payload - 2:
|
||||||
pkts.append(bytes([ts_hi]) + rest[:max_payload - 1])
|
pkts.append(bytes([ts_hi]) + rest[: max_payload - 1])
|
||||||
rest = rest[max_payload - 1:]
|
rest = rest[max_payload - 1 :]
|
||||||
if rest: # 结束包: header + ts + data..F7
|
if rest:
|
||||||
pkts.append(bytes([ts_hi, ts_lo]) + rest)
|
pkts.append(bytes([ts_hi, ts_lo]) + rest)
|
||||||
return pkts
|
return pkts
|
||||||
|
|
||||||
|
|
||||||
def ble_midi_decode_packet(payload: bytes) -> bytes:
|
def ble_midi_decode_packet(payload: bytes) -> bytes:
|
||||||
"""单个 BLE-MIDI 通知包 -> MIDI 字节流 (仅 SysEx 场景: 数据均为 7bit)"""
|
|
||||||
if len(payload) < 2:
|
if len(payload) < 2:
|
||||||
return b""
|
return b""
|
||||||
i = 1 # 跳过 header
|
i = 1
|
||||||
if payload[i] & 0x80: # 时间戳(起始/结束包); 续包无
|
if payload[i] & 0x80:
|
||||||
i += 1
|
i += 1
|
||||||
return bytes(payload[i:])
|
return bytes(payload[i:])
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------- BLE transport
|
|
||||||
class BleMidiSim:
|
class BleMidiSim:
|
||||||
"""与 AppSim 同接口: send / read_frame / drain / query / close"""
|
"""与 AppSim 同接口: send / read_frame / drain / query / close"""
|
||||||
|
|
||||||
def __init__(self, name=DEFAULT_NAME, address=None, scan_timeout=15.0):
|
def __init__(
|
||||||
|
self,
|
||||||
|
name=DEFAULT_NAME,
|
||||||
|
address=None,
|
||||||
|
scan_timeout=20.0,
|
||||||
|
transport="midi",
|
||||||
|
unpair=False,
|
||||||
|
):
|
||||||
from bleak import BleakClient, BleakScanner
|
from bleak import BleakClient, BleakScanner
|
||||||
|
|
||||||
self._BleakClient = BleakClient
|
self._BleakClient = BleakClient
|
||||||
self._BleakScanner = BleakScanner
|
self._BleakScanner = BleakScanner
|
||||||
self.parser = FrameParser()
|
self.parser = FrameParser()
|
||||||
self.pending = []
|
self.pending = []
|
||||||
self._rx = queue.Queue()
|
self._rx = queue.Queue()
|
||||||
self._client = None
|
self._client = None
|
||||||
self._char = None
|
self._write_char = None
|
||||||
|
self._notify_chars = []
|
||||||
|
self._transport = transport
|
||||||
|
self._unpair = unpair
|
||||||
self._loop = asyncio.new_event_loop()
|
self._loop = asyncio.new_event_loop()
|
||||||
self._thread = threading.Thread(target=self._loop.run_forever,
|
self._thread = threading.Thread(target=self._loop.run_forever, daemon=True)
|
||||||
daemon=True)
|
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
self.info = self._run(self._connect(name, address, scan_timeout))
|
self.info = self._run(self._connect(name, address, scan_timeout))
|
||||||
|
|
||||||
# ---- async helpers ----
|
def _run(self, coro, timeout=90.0):
|
||||||
def _run(self, coro, timeout=60.0):
|
|
||||||
return asyncio.run_coroutine_threadsafe(coro, self._loop).result(timeout)
|
return asyncio.run_coroutine_threadsafe(coro, self._loop).result(timeout)
|
||||||
|
|
||||||
async def _connect(self, name, address, scan_timeout):
|
async def _find(self, name, address, scan_timeout):
|
||||||
dev = None
|
|
||||||
if address:
|
if address:
|
||||||
dev = await self._BleakScanner.find_device_by_address(
|
return await self._BleakScanner.find_device_by_address(
|
||||||
address, timeout=scan_timeout)
|
address, timeout=scan_timeout
|
||||||
else:
|
)
|
||||||
print(f"扫描 BLE 设备 ({scan_timeout:.0f}s) ...")
|
print(f"扫描 BLE 设备 ({scan_timeout:.0f}s) ...")
|
||||||
devs = await self._BleakScanner.discover(
|
# name filter is more reliable than service UUID filter on Windows
|
||||||
timeout=scan_timeout, service_uuids=[MIDI_SERVICE])
|
t0 = time.monotonic()
|
||||||
for d in devs:
|
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}")
|
print(f" 发现: {d.name!r} {d.address}")
|
||||||
if d.name and name.lower() in d.name.lower():
|
return d
|
||||||
dev = d
|
print(" ...")
|
||||||
if dev is None and devs:
|
return None
|
||||||
dev = devs[0]
|
|
||||||
print(f" (未匹配名称 {name!r}, 使用第一个 MIDI 设备)")
|
|
||||||
if dev is None:
|
|
||||||
raise RuntimeError(f"未找到 BLE-MIDI 设备 {name!r}")
|
|
||||||
|
|
||||||
client = self._BleakClient(dev)
|
async def _connect(self, name, address, scan_timeout):
|
||||||
await client.connect()
|
dev = await self._find(name, address, scan_timeout)
|
||||||
char = None
|
if dev is None:
|
||||||
for svc in client.services:
|
raise RuntimeError(f"未找到设备 {name!r}")
|
||||||
if svc.uuid.lower() == MIDI_SERVICE.lower():
|
|
||||||
for c in svc.characteristics:
|
if self._unpair:
|
||||||
if c.uuid.lower() == MIDI_CHAR.lower():
|
|
||||||
char = c
|
|
||||||
if char is None:
|
|
||||||
raise RuntimeError("设备无 BLE-MIDI 特征")
|
|
||||||
try:
|
try:
|
||||||
max_payload = char.max_write_without_response_size
|
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:
|
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
|
max_payload = DEFAULT_MTU_PAYLOAD
|
||||||
if not max_payload or max_payload < DEFAULT_MTU_PAYLOAD:
|
for svc in client.services:
|
||||||
max_payload = DEFAULT_MTU_PAYLOAD
|
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):
|
def _on_notify(_sender, data):
|
||||||
self._rx.put(bytes(data))
|
self._rx.put(bytes(data))
|
||||||
|
|
||||||
await client.start_notify(char, _on_notify)
|
for u in notify_list:
|
||||||
self._client, self._char = client, char
|
try:
|
||||||
self._max_payload = max_payload
|
await client.start_notify(u, _on_notify)
|
||||||
return {"name": dev.name, "address": dev.address,
|
self._notify_chars.append(u)
|
||||||
"max_payload": max_payload}
|
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):
|
async def _disconnect(self):
|
||||||
if self._client is not None:
|
if self._client is None:
|
||||||
|
return
|
||||||
|
for u in self._notify_chars:
|
||||||
try:
|
try:
|
||||||
await self._client.stop_notify(self._char)
|
await self._client.stop_notify(u)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
await self._client.disconnect()
|
await self._client.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# ---- AppSim-compatible sync API ----
|
|
||||||
def close(self):
|
def close(self):
|
||||||
try:
|
try:
|
||||||
self._run(self._disconnect(), timeout=10)
|
self._run(self._disconnect(), timeout=10)
|
||||||
|
|
@ -157,12 +215,27 @@ class BleMidiSim:
|
||||||
data = self._rx.get_nowait()
|
data = self._rx.get_nowait()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
break
|
break
|
||||||
self.pending.extend(self.parser.feed(ble_midi_decode_packet(data)))
|
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):
|
def send(self, frame: bytes):
|
||||||
for pkt in ble_midi_encode_sysex(frame, self._max_payload):
|
if self._transport == "midi":
|
||||||
self._run(self._client.write_gatt_char(
|
pkts = ble_midi_encode_sysex(frame, self._max_payload)
|
||||||
self._char, pkt, response=False), timeout=10)
|
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):
|
def read_frame(self, timeout=1.0, want_dev=False):
|
||||||
deadline = time.monotonic() + timeout
|
deadline = time.monotonic() + timeout
|
||||||
|
|
@ -187,18 +260,20 @@ class BleMidiSim:
|
||||||
return self.read_frame(timeout=timeout)
|
return self.read_frame(timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------- report
|
def write_report(R: Results, info: dict, path: str, notes: list[str] | None = None):
|
||||||
def write_report(R: Results, info: dict, path: str):
|
n = {
|
||||||
n = {s: sum(1 for r in R.rows if r[1] == s)
|
s: sum(1 for r in R.rows if r[1] == s)
|
||||||
for s in ("PASS", "FAIL", "SKIP", "SENT")}
|
for s in ("PASS", "FAIL", "SKIP", "SENT")
|
||||||
|
}
|
||||||
lines = [
|
lines = [
|
||||||
"# BLE MIDI SysEx 协议测试报告",
|
"# BLE SysEx 协议测试报告",
|
||||||
"",
|
"",
|
||||||
f"- 日期: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}",
|
f"- 日期: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}",
|
||||||
f"- 设备: {info.get('name')} ({info.get('address')})",
|
f"- 设备: {info.get('name')} ({info.get('address')})",
|
||||||
f"- 传输: BLE-MIDI / bleak, 单包载荷 {info.get('max_payload')} 字节",
|
f"- 传输: {info.get('transport')} / bleak, payload={info.get('max_payload')}",
|
||||||
|
f"- notify: {info.get('notify')}",
|
||||||
"- 协议来源: Doc/指令测试.docx",
|
"- 协议来源: Doc/指令测试.docx",
|
||||||
"- 测试脚本: tools/test_protocol_ble.py (复用 test_protocol_app_sim.py 用例)",
|
"- 测试脚本: tools/test_protocol_ble.py",
|
||||||
"",
|
"",
|
||||||
"## 汇总",
|
"## 汇总",
|
||||||
"",
|
"",
|
||||||
|
|
@ -208,21 +283,18 @@ def write_report(R: Results, info: dict, path: str):
|
||||||
"",
|
"",
|
||||||
"## 明细",
|
"## 明细",
|
||||||
"",
|
"",
|
||||||
"| # | 用例 | 结果 | 详情(RX/说明) |",
|
"| # | 用例 | 结果 | 详情 |",
|
||||||
"|---|------|------|----------------|",
|
"|---|------|------|------|",
|
||||||
]
|
]
|
||||||
for i, (name, status, detail) in enumerate(R.rows, 1):
|
for i, (name, status, detail) in enumerate(R.rows, 1):
|
||||||
lines.append(f"| {i} | {name} | {status} | {detail} |")
|
lines.append(f"| {i} | {name} | {status} | {detail} |")
|
||||||
|
lines += ["", "## 备注", ""]
|
||||||
|
for note in notes or []:
|
||||||
|
lines.append(f"- {note}")
|
||||||
lines += [
|
lines += [
|
||||||
"",
|
"- `01 03` 为 MCU 实际版本编码;`01 FF` 暂 `BRS08L`(文档待定)。",
|
||||||
"## 备注",
|
"- `04 xx`/`06 xx` 无应答,SENT 表示已发送。",
|
||||||
"",
|
"- `FD 01` 升级指令不在本协议范围。",
|
||||||
"- `01 03` 固件主版本回复为 MCU 实际版本 (0.2.5 -> 00 02 05 01),",
|
|
||||||
" 与文档样例 `21 05 7F 01` 编码不同 (文档未给编码规则)。",
|
|
||||||
"- `01 FF` 固件编码暂回 ASCII `BRS08L`, 文档标注待定。",
|
|
||||||
"- `04 xx`/`06 xx` 为无应答下发命令, SENT 表示已发送。",
|
|
||||||
"- `FD 01` 升级指令不属于本 UART4/BLE 应用协议范围, 未测。",
|
|
||||||
"- 蓝牙广播名由 Dream 模块固件决定 (Smart Guitar MIDI), 与 `01 02` 协议设备名无关。",
|
|
||||||
]
|
]
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
|
@ -230,39 +302,52 @@ def write_report(R: Results, info: dict, path: str):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------- selftest
|
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():
|
def selftest():
|
||||||
R = Results()
|
R = Results()
|
||||||
# 短帧: 单包
|
|
||||||
pkts = ble_midi_encode_sysex(bytes.fromhex("F0600101F7"), 20)
|
pkts = ble_midi_encode_sysex(bytes.fromhex("F0600101F7"), 20)
|
||||||
ok = pkts == [bytes.fromhex("8080F0600101F7")]
|
ok = pkts == [bytes.fromhex("8080F0600101F7")]
|
||||||
R.add("encode short", "PASS" if ok else "FAIL",
|
R.add("encode short", "PASS" if ok else "FAIL", str([hexs(p) for p in pkts]))
|
||||||
str([hexs(p) for p in pkts]))
|
|
||||||
# 长帧: 47B -> 多包, 且可解码还原
|
|
||||||
frame = bytes([0xF0, 0x60, 0x02, 0x01]) + bytes(range(1, 43)) + bytes([0xF7])
|
frame = bytes([0xF0, 0x60, 0x02, 0x01]) + bytes(range(1, 43)) + bytes([0xF7])
|
||||||
pkts = ble_midi_encode_sysex(frame, 20)
|
pkts = ble_midi_encode_sysex(frame, 20)
|
||||||
back = b"".join(ble_midi_decode_packet(p) for p in pkts)
|
back = b"".join(ble_midi_decode_packet(p) for p in pkts)
|
||||||
ok = back == frame and len(pkts) >= 3
|
R.add(
|
||||||
R.add("encode/decode 47B roundtrip", "PASS" if ok else "FAIL",
|
"encode/decode 47B",
|
||||||
f"{len(pkts)} pkts")
|
"PASS" if back == frame and len(pkts) >= 3 else "FAIL",
|
||||||
# 解码: 单包带时间戳
|
f"{len(pkts)} pkts",
|
||||||
ok = ble_midi_decode_packet(bytes.fromhex("8080F0600101F7")) == bytes.fromhex("F0600101F7")
|
)
|
||||||
R.add("decode single pkt", "PASS" if ok else "FAIL", "")
|
|
||||||
# 解码: 续包(无时间戳)
|
|
||||||
ok = ble_midi_decode_packet(bytes.fromhex("80010203")) == bytes.fromhex("010203")
|
|
||||||
R.add("decode continuation", "PASS" if ok else "FAIL", "")
|
|
||||||
return R.summary()
|
return R.summary()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
ap = argparse.ArgumentParser(description="BLE-MIDI 吉他协议全量测试")
|
ap = argparse.ArgumentParser(description="BLE 吉他协议全量测试")
|
||||||
ap.add_argument("--ble-name", default=DEFAULT_NAME,
|
ap.add_argument("--ble-name", default=DEFAULT_NAME)
|
||||||
help=f"广播名匹配 (默认 {DEFAULT_NAME!r})")
|
ap.add_argument("--ble-address")
|
||||||
ap.add_argument("--ble-address", help="直接按 MAC/地址连接")
|
ap.add_argument(
|
||||||
ap.add_argument("--allow-poweroff", action="store_true",
|
"--transport",
|
||||||
help="允许执行 05 00 关机用例")
|
choices=("midi", "uart"),
|
||||||
ap.add_argument("--selftest", action="store_true", help="无硬件自检")
|
default="midi",
|
||||||
ap.add_argument("--report", help="报告输出路径 (默认 Doc/reports/ble_sysex_<时间戳>.md)")
|
help="midi=标准 BLE-MIDI 帧; uart=自定义 e49a 原始 SysEx",
|
||||||
|
)
|
||||||
|
ap.add_argument("--unpair", action="store_true", help="连接前先 unpair Windows 配对")
|
||||||
|
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()
|
args = ap.parse_args()
|
||||||
|
|
||||||
if args.selftest:
|
if args.selftest:
|
||||||
|
|
@ -272,21 +357,40 @@ def main():
|
||||||
except ImportError:
|
except ImportError:
|
||||||
sys.exit("缺少 bleak: pip install bleak")
|
sys.exit("缺少 bleak: pip install bleak")
|
||||||
|
|
||||||
sim = BleMidiSim(name=args.ble_name, address=args.ble_address)
|
sim = BleMidiSim(
|
||||||
print(f"=== BLE 协议测试开始: {sim.info['name']} ({sim.info['address']}) "
|
name=args.ble_name,
|
||||||
f"payload={sim.info['max_payload']} ===")
|
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')} ==="
|
||||||
|
)
|
||||||
|
notes = [
|
||||||
|
f"transport={sim.info.get('transport')}",
|
||||||
|
"若 FAIL 且 timeout: 检查手机 App 是否占用、Windows 是否残留配对(--unpair)、吉他是否需完全断电重启。",
|
||||||
|
"实测: 标准 BLE-MIDI 可稳定连接但 SysEx 无应答; 自定义 uart 写 raw SysEx 会断连。",
|
||||||
|
]
|
||||||
try:
|
try:
|
||||||
R = run_tests(sim, allow_poweroff=args.allow_poweroff)
|
R = smoke(sim) if args.smoke else run_tests(sim, allow_poweroff=args.allow_poweroff)
|
||||||
finally:
|
finally:
|
||||||
sim.close()
|
sim.close()
|
||||||
code = R.summary()
|
code = R.summary()
|
||||||
|
|
||||||
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
path = args.report or os.path.join(
|
path = args.report or os.path.normpath(
|
||||||
|
os.path.join(
|
||||||
os.path.dirname(os.path.abspath(__file__)),
|
os.path.dirname(os.path.abspath(__file__)),
|
||||||
"..", "..", "..", "Doc", "reports", f"ble_sysex_{ts}.md")
|
"..",
|
||||||
path = os.path.normpath(path)
|
"..",
|
||||||
write_report(R, sim.info, path)
|
"..",
|
||||||
|
"Doc",
|
||||||
|
"reports",
|
||||||
|
f"ble_sysex_{ts}.md",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
write_report(R, sim.info, path, notes=notes)
|
||||||
print(f"\n报告已写入: {path}")
|
print(f"\n报告已写入: {path}")
|
||||||
sys.exit(code)
|
sys.exit(code)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue