Skill:TSMaster 干扰仪上位机小程序开发
> 目标:在一个 TSMaster MiniProgram 里,把总线干扰仪做成「能连、能配、能干扰、能判定、能收尾」的上位机。
> 所有参数都是真机跑出来的值,不是照抄 demo;demo 与我们环境的差异见 VALIDATION.md。
0. 何时用
| 用户怎么说 | 去向 |
|---|---|
| 「写个干扰仪控制程序」「让小程序控制干扰仪发干扰」 | §2 架构 + TEMPLATES.md §2~§4 |
| 「做个 Bus-off 用例」「干扰 ACK / EOF 字段验证恢复」 | TEMPLATES.md §7、REFERENCE.md §8 |
| 「测采样点」「采样点测不出来 / 波动大 / 返回 -2」 | TEMPLATES.md §5、TROUBLESHOOTING.md §3 |
| 「设备连不上」「报 -18 / -7 / -12」 | TROUBLESHOOTING.md §2 |
| 「前几条用例过了,后面全 -18」 | TROUBLESHOOTING.md §5(句柄泄漏) |
| 「干扰计数一直是 0」「配了没效果」 | TROUBLESHOOTING.md §4 |
| 「dll 加载失败 / 报 126 / 报 193」 | TROUBLESHOOTING.md §1 |
1. 铁律速查表
| # | 铁律 | 违反后果 |
|---|---|---|
| 1 | libTSDevBase.dll 必须 32 位,且与 TSMaster.exe 同目录 |
报 193(ERROR_BAD_EXE_FORMAT)加载失败 |
| 2 | 加载前先 SetDllDirectoryA(dll 目录),用 LoadLibraryExA(..., LOAD_WITH_ALTERED_SEARCH_PATH),用完还原 |
报 126,依赖 libusb-1.0_tosun.dll 找不到 |
| 3 | 不写死 dll 绝对路径,按 §TEMPLATES 3 的五级顺序查找,失败时打印全部尝试过的路径 |
换机器必崩,且现场无法排查 |
| 4 | 采样点测量(6~10 秒阻塞)绝不在 step 内同步调用,必须线程 + 5ms 轮询 | 卡住看门狗(3 秒),停止按钮失灵 |
| 5 | 长时程操作(干扰 10 秒、观测 60 秒)用 5ms tick 状态机推进一步,不开线程 | 开线程反而难中止、难观测中间状态 |
| 6 | 线程入口签名固定 DWORD WINAPI 名(LPVOID),必须留在全局定义区 |
放进自定义函数区 → 编译过但 UI 不显示 / 链接失败 |
| 7 | 跨线程共享只用 volatile LONG + Interlocked* 系列 |
结果读到半个结构体,偶发错判 |
| 8 | 先写结果结构体,最后才置 done = 1 |
主线程读到未写完的结果 |
| 9 | 连接成功后必须 g_th_handle = h;,否则 disconnect 空转 → 每次漏一条 TCP |
控制端口耗尽,后续全部 -18 |
| 10 | 配置干扰前先用 times_query_t{clear_flag=1} 清计数 |
报告里的干扰次数带上一轮累计值 |
| 11 | Frame_Field 只用 0~6,7(帧间间隔)与 8(错误字段)实测不生效 |
配了没反应,误判成硬件坏 |
| 12 | guest_arbit_sample_pos 必须低于被测件采样点(如 78 vs 80) |
填成相同的 80 会落在边界,测不出结果 |
| 13 | 开 MissBit_trigger 后位置超界会继续向后计数 |
CRC 第 17 bit 实际打到 ACK 界定符,判定全错 |
| 14 | 每个失败点都要记 错误码 + 阶段,worker 里不许硬写 -1 |
真实原因丢失,只剩「失败」两个字 |
| 15 | 每个函数 try { } catch (...) 兜底,catch 里记日志并 app.terminate_application() |
未捕获异常直接崩宿主进程,无日志 |
| 16 | 空壳 / 未实现用例返回 TC_NA,绝不返回成功 |
报告出现「假绿」 |
2. 程序架构(四层 + 区块落位)
┌─ L4 编排层:自定义函数 + 用例 tc_* ← 对 UI 暴露的入口
├─ L3 能力层:init / disturb_config / sp_test / stop / query
├─ L2 连接层:create → scan → get_info → open;ETH/USB 回退;句柄管理
└─ L1 加载层:dll 定位 + 函数指针表
区块落位(放错区块是最常见的「编译过了但 UI 里没有」):
| 内容 | 必须放在 |
|---|---|
类型定义、函数指针、全局状态、worker 线程函数 |
Global_Definitions |
| 对 UI 暴露的能力函数(load / connect / sp_start / busoff_run / abort / err_text) | Custom_Function,一区块一函数 |
用例 tc_* |
用例区,登记进用例表才可勾选 |
| 跨区块调用 | Global_Definitions 里加前置声明 |
自定义函数四条硬性要求(缺一条 UI 里就不显示):
// CODE BLOCK BEGIN Custom_Function <函数名> <base64标识>
// 自定义函数 "<函数名>"
s32 <函数名>(void) { try { // 自定义函数:
return 0;
} catch (...) { log_nok("CRASH detected"); app.terminate_application(); return(IDX_ERR_MP_CODE_CRASH); }}
// CODE BLOCK END Custom_Function <函数名>
一个区块只能一个函数;签名必须 s32 名(void);首行标记 // 自定义函数: 要在 { try { 同一行;catch 必须返回 IDX_ERR_MP_CODE_CRASH。
参数怎么传(自定义函数不支持参数):少量标量走全局变量;指针/句柄用 s32 承载(32 位同宽,安全);字符串返回静态常量地址;线程入口不能迁。
3. 两种耗时模型(选型先看这张表)
| 场景 | 单次耗时 | 用哪个模型 | 原因 |
|---|---|---|---|
| 采样点测量 | 6~10 秒,一次调用内跑完 | 独立线程 + 5ms 轮询 | SDK 内部循环,没有中间状态可观测 |
| Bus-off 干扰 | 10 秒干扰 + 60 秒观测 | 5ms tick 状态机 | 有中间状态(干扰中/观测中)、要能中止、要能每秒打日志 |
| 单次干扰脉冲 | < 1 秒 | 直接调用 | 远低于看门狗阈值,没必要异步 |
| 扫描设备 / 打开设备 | 1~5 秒 | 线程或放在连接阶段一次性做 | 同采样点,属于阻塞调用 |
模型 A:线程 + 轮询(起始 → 轮询 → 工作线程三段式)
s32 th_sp_start(void) { try { // ① 启动后立即返回
if (InterlockedCompareExchange(&g_th_busy, 1, 0) != 0) return 0;
InterlockedExchange(&g_th_job_done, 0);
HANDLE th = CreateThread(NULL, 0, th_sp_worker, NULL, 0, NULL);
if (!th) { InterlockedExchange(&g_th_busy, 0); return -1; }
CloseHandle(th);
return 1;
} catch (...) { ... } }
s32 th_sp_poll(void) { try { // ② 主线程每 5ms 查一次
return (s32)InterlockedCompareExchange(&g_th_job_done, 1, 1);
} catch (...) { ... } }
模型 B:tick 状态机(步进式,每 tick 推进一步)
if (g_step == STEP_IDLE) { ...初始化...; g_step = STEP_START; return 0; }
if (g_step == STEP_START) { ...连接+配干扰...; g_step = STEP_RUN; return 0; }
if (g_step == STEP_RUN) { g_tick++; if (g_tick >= 2000) { g_step = STEP_OBSERVE; } return 0; } // 200 tick = 1 秒
if (g_step == STEP_OBSERVE) { ...判定...; g_step = STEP_DONE; return 1; }
return 1; // DONE
> 5ms 一个 tick,200 tick = 1 秒。不要用 Sleep() 等,那会顶死调度器。
4. 标准流程(8 步)
1. 环境自检
- dll 是 32 位?依赖 libusb-1.0_tosun.dll 在不在?
- 控制端口 21840 有没有残留连接?(Get-NetTCPConnection -RemotePort 21840)
2. 加载 dll
- 五级查找 → SetDllDirectory → LoadLibraryEx → GetProcAddress
- 必查 4 个:create / scan / open / sp_test(缺一就报 -2 并打印缺失项)
3. 连接设备
- create(dev_type=0, com=1 eth / 4 usb) → scan → get_info → open(uid)
- 自动模式:ETH 失败回退 USB,保留首个错误码
4. 初始化 CAN 控制器
- th_can_ctrl_t:仲裁/数据波特率 + 两个采样点(默认 500k/2M/80/80)
5. 选择测量方式
- 采样点 → 线程模型;干扰 → 先清计数再配干扰
6. 编排与推进
- 线程模型:用例里 tc_begin → start → 轮询 → 判定
- tick 模型:用例每 step 调引擎一次,引擎自己推进内部相位
7. 判定与回写
- 测量类:数值优先,错误码只进「诊断」栏
- 干扰类:先查干扰次数 > 0(否则总线无报文),再查通信是否恢复
8. 收尾(顺序不能错)
- stop(停止干扰) → close → destory → 句柄置空 → 再查 21840 应为空
5. 验证清单(每次改完必跑)
[ ] 1. 编译:clang 直接编,exit 0,bin/<程序名>.mp 时间戳更新
[ ] 2. 定义唯一性:无重复 s32 xxx(void) { try 定义(混用脚本改与区块写的高发区)
[ ] 3. 括号平衡:{ 数量 == } 数量
[ ] 4. 跑前清场:21840 无 Established 连接
[ ] 5. 只读先验:先跑只读探针确认连接与 dll 版本,不做任何干扰
[ ] 6. 跑测试:干扰类会真的产生错误帧,确认目标是台架不是实车
[ ] 7. 看报告:PASS/FAIL/NA 计数符合预期,诊断栏有中文原因
[ ] 8. 跑后查泄漏:21840 仍应为空
第 4 步和第 8 步必须前后各查一次,才能证明「修好了」而不是「这次没报错」。
6. 排错速查
| 现象 | 最可能的原因 |
|---|---|
| UI 里看不到自定义函数 | 区块不符合注册规范(§2 四条) |
找不到标识符 |
跨区块调用没加前置声明 |
cannot jump from this goto |
clang 禁止 goto 跳过变量初始化 |
返回 -18 |
设备被其它程序占用(含 TSMaster 仪器设备管理器) |
scan 扫到但 open 失败 |
同上,不是没接线 |
| 采样点全 0 且返回 0 | 总线无报文,或 guest_* 不低于被测件 |
| 干扰计数一直是 0 | 总线无报文(干扰次数依赖真实帧) |
| 前几条过、后面全 -18 | 句柄没存全局,TCP 泄漏 |
| 0xC0000005 直接崩 | 手写结构体尺寸与 SDK 不符 |
详见 TROUBLESHOOTING.md。
7. 参考文件
| 文件 | 内容 |
|---|---|
REFERENCE.md |
导出函数表、结构体尺寸、枚举、错误码、实测参数 |
TEMPLATES.md |
可直接复制的 12 段骨架 |
TROUBLESHOOTING.md |
六类现象 → 根因 → 处置 → 验证 |
VALIDATION.md |
哪条实测、哪条继承、哪条没覆盖 |
ASSETS/check_env.ps1 |
设备环境自检:dll 位数、依赖、控制端口连接 |