PC 端脚本
新建和编辑
导入 DBC/LDF 后,在“管理节点”中将需要仿真的数据库节点加入总线;未分配节点不参与周期发送、LIN 响应或脚本运行。从总线移除数据库节点会保留状态、脚本和坐标,再次添加时恢复。旧工程中操作过的数据库节点自动保留在总线上。
在“总线配置”的 CAN / LIN 画布添加自定义节点,或打开数据库节点的“节点设置”。新节点默认开启,可在“状态”中切换。每个节点可绑定一个脚本:选择“新建…”输入名称,或“选择…”已有 TypeScript / JavaScript 文件。移除脚本只解除绑定,不删除文件。自定义节点至少连接 CAN、LIN 中的一条总线。
卡片底部依次为编辑或添加脚本、节点设置、节点面板(仅数据库节点)。选中卡片后,空格切换激活,Enter 打开设置,Delete 删除自定义节点,Shift+F10 打开右键菜单;左侧树的空格只选中节点。未激活节点使用虚线卡片和连接线。右键“编译”只检查当前节点,结果写入 Write;删除节点需要确认,脚本文件保留。
脚本路径相对于 .blproj 所在目录。复制工程时同时复制脚本及其依赖。点击“编辑”使用系统默认程序打开文件;用 VS Code 打开整个工程目录可获得类型提示。
新建或添加脚本后会生成 tsconfig.json 和 node_modules/@types/beinilink/index.d.ts,包括变量全名、变量类型、CAN/LIN 帧名和信号名。变量或数据库配置变化后自动更新声明。已有 tsconfig 的配置和注释保留,只追加 files;如果自定义了 compilerOptions.types,需自行加入 node 和 beinilink。
启动、停止和错误
开始测量时只编译并运行总线上已激活节点的脚本;这些脚本的语法错误会阻止启动,Write 窗口显示节点名和 文件:行:列。未激活节点不参与启动编译,Write 记录 Script: 节点名、skipped (inactive)。节点设置中的“编译”检查当前所选文件,无需先点“确定”,并显示成功或错误数;更换路径后清除结果。编译只转换和打包,类型检查由编辑器完成。测量需要至少打开一个硬件端口。测量期间节点分配只读,已有脚本仍可在编辑器中打开;挂了脚本的节点不能切换激活,无脚本数据库节点仍可切换周期发送。
每个节点独立运行于 Node.js Worker。同一文件可绑定多个节点,通过只读的 NODE_NAME 区分实例。节点只能收发和访问信号所属的已连接总线;变量和按键事件由所有节点共享。变量启动后依次执行 Util.Init;停止时先执行 Util.End,每个节点最多等待 2 秒,再终止 Worker。未捕获异常只终止出错节点的脚本,其他节点和测量继续。Write 显示日志和异常,节点卡片显示运行状态。旧工程中的全局脚本会迁移为同时连接 CAN、LIN 的自定义节点。
只运行自己信任的脚本。PC 脚本拥有完整 Node.js 文件和网络权限。此功能与设备端 Python REPL 分开。
用滑块发送 CAN
示例目录的 script.blproj 与 script.ts 放在同一目录。打开工程,选择实际设备并核对 500 kbit/s CAN 配置,打开 Lamp 面板和 Trace,开始测量。拖动滑块会发送 ID 0x321,第一个字节为 Lamp.Level(0~100)。接收 0x123 则用第一个字节更新变量与面板,也会触发该变量的发送回调。
Util.OnVar('Lamp.Level', async ({ value }) => {
await output({
id: 0x321, dir: 'OUT', data: Buffer.from([value]),
msgType: { idType: CAN_ID_TYPE.STANDARD, canfd: false, brs: false, remote: false }
});
});
Util.OnCan(0x123, msg => {
if (msg.data.length) setVar('Lamp.Level', Math.min(100, msg.data[0]));
});
Util.Init(() => console.log('Level', getVar('Lamp.Level')));
Util.End(() => console.log('Stopped'));output() 返回从脚本启动起计算的主机时间(微秒),表示发送请求已被后端接受,不代表总线 ACK 的硬件时间。输入帧的 ts 使用可用的设备时间戳,缺失时为 undefined。
信号和变量
信号使用 消息名.信号名;DBC 与 LDF 重名时用 CAN.消息名.信号名 或 LIN.帧名.信号名。例如数据库中有 Status.Speed 时:
Util.OnSignal('Status.Speed', signal => console.log(signal.physValue));
console.log(getSignal('Status.Speed').rawValue);
Util.Init(async () => {
await setSignal('Status.Speed', 100); // 原始数值
await setSignal('Status.Speed', '25'); // 物理值,也可使用枚举名称
});getSignal 读取该 Worker 解码的最新值,收到帧前返回数据库初始值。OnSignal 每次解码对应帧时调用,参数含原始值和物理值。setSignal 需要测量运行,走面板的信号写入路径:更新仿真数据,是否发送取决于节点、报文周期和 LIN 调度设置。
getVar 同步读取,setVar / setVars 先校验并更新本地镜像,再发送到共享变量运行时。本地校验失败会抛出异常;主进程拒绝写入时只在 Write 记录警告,脚本继续运行。变量名使用完整命名空间。系统变量只读。Util.OnVar('*', ({name, value}) => ...) 监听所有变量。
常用事件与 LIN
OnCan / OnLin 接受 ID、帧名或 true(全部);OnSignal、OnVar 和 OnKey 也各有 Once 和 Off 版本。取消监听时传入注册时的同一个函数。只转发脚本注册的总线帧,持续处理不过来的 Worker 会报错退出。
const onFrame = (msg: CanMessage) => console.log(msg.data.toString('hex'));
Util.OnCan(0x123, onFrame);
Util.OffCan(0x123, onFrame);
Util.OnKey('x', () => console.log('x'));按键在测量期间转发;输入框、文本编辑区、下拉框、对话框和 Esc 不转发。Node.js 的定时器以及 console.log/warn/error 可直接使用。
Util.Init(async () => {
await output({ frameId: 0x12, data: Buffer.from([1, 2]),
direction: LinDirection.SEND, checksumType: LinChecksumType.ENHANCED });
await linPowerCtrl(true);
await linStartScheduler('Normal');
});
Util.End(async () => { await linStopScheduler(); });LIN 调度要求已打开的主节点和工程 LDF 中的调度表。请求帧使用 LinDirection.RECV,data 长度指定期望的 1~8 字节。当前设备接口不支持 RECV_AUTO_LEN。供电接口只适用于支持 LIN 供电控制的设备。
支持 import { Util, output } from 'ECB' 或 'beinilink'。本期不包含 UDS、SOME/IP、DoIP、测试框架和串口脚本 API。
刷写 ECU
示例用 PC 脚本通过 CAN、LIN 对自带 Bootloader 软件的产品进行刷写,刷写流程都写在脚本中,两个工程共用 flash-uds.ts:
- CAN:flash.blproj 与 flash.ts,经典 CAN,ISO-TP。
- LIN:flash-lin.blproj 与 flash-lin.ts,主节点用
0x3C发送、0x3D轮询响应,经典校验。不要同时运行调度表。
把固件命名为 firmware.bin 放在工程目录,按 ECU 的 Bootloader 修改脚本开头的 config,核对波特率后开始测量,按 f 刷写,进度显示在 Write。脚本文件要放在同一目录。
| 步骤 | 服务 | 说明 |
|---|---|---|
| 1 | 10 03、10 02 | 进入扩展会话和编程会话 |
| 2 | 27 11、27 12 | 用 AES-128 ECB 加密 16 字节 Seed,结果作为 Key 发送 |
| 3 | 31 01 FF00 | 擦除,参数为地址和长度 |
| 4 | 34、36、37 | 按 ECU 返回的块长度下载 |
| 5 | 31 01 0202 | 发送整个固件的 CRC32,ECU 返回的状态字节为 0 表示通过 |
| 6 | 11 01 | 复位 ECU |
任一步失败都会停止,Write 显示负响应码,校验不通过时不复位 ECU。服务 ID、例程 ID 和参数格式需要与 Bootloader 一致。示例中的 AES 密钥只用于演示。
监听 MeLiBu
导入 MBDF,在 MeLiBu 总线上分配节点,再到“节点设置”绑定脚本并开启节点。新建、选择、编译沿用 CAN/LIN 的操作。生成的类型包括 MelibuFrameName、MelibuNodeName 和 MBDF 信号名。只有连接到 MeLiBu 的节点可以调用这些接口。
Util.OnMelibu('GetVoltage', msg => {
if (msg.dir === 'IN' && msg.crc === 'ok') console.log(getSignal('Voltage').physValue);
});
Util.OnSignal('Voltage', signal => console.log(signal.rawValue, signal.physValue));OnMelibu / OnMelibuOnce / OffMelibu 接受 MBDF 帧名、16 位原始 ID 或 true。data 是 Buffer;主机发出为 OUT,从机回复为 IN。读请求头与回复是两个事件。匹配多个帧时返回 candidates,不擅自填写 frame;CRC 错和截断数据仍可观察。没有已知时间戳时不填写 ts。只有 ACK 的事件保留在 Trace,不当作数据帧回调。
信号全名使用 MELIBU.Voltage;短名 Voltage 只在整个信号目录中唯一时可用。CAN/LIN 的全名仍为 CAN.消息名.信号名、LIN.帧名.信号名。getSignal 同步读取原生 runtime 的镜像,OnSignal 接收计划、脚本收发和节点窗口编辑产生的原生更新。MBDF 的原始值和初值均为十进制字符串,完整保留 64 位。物理编码有效时返回 physValue;知道来源帧时返回 updatedFrom。
发送和读取 MeLiBu
await setSignal('Level', 100);
await melibuSend('SetLight', 'Lamp', { Level: 200 });
await melibuSend('SetCounter', 'Lamp', { Counter: '18446744073709551615' });
const response = await melibuRead('GetVoltage', 'Lamp');
console.log(response.data, getSignal('Voltage').physValue);参数按信号名填写,未填写的信号在请求实际执行时取共享当前值。安全整数和无符号十进制整数字符串表示 raw;其他数值字符串表示物理值,整数物理值可写成 '25.0',也支持逻辑编码标签。MBDF 的 setSignal 使用相同规则;CAN/LIN 的字符串转换规则保持前文所述。
melibuSend 只发送 M2S 帧,目标可以是 MBDF 节点或 Broadcast / Broadcast0~Broadcast3。第四个参数可覆盖 { subAddress, instruction },固定头字段不能修改。melibuRead 只读 S2M 帧,目标必须是单个节点。有效回复会在 Promise 完成前更新共享信号;CRC 错、超时和传输错误分别 reject melibu.crc、melibu.timeout、melibu.transport,失败时保留旧信号值。ACK 失败使用 melibu.noAck 或 melibu.nack。
发送原始 M2S 帧使用 output({ rawId, data: Buffer, kind: 'DATA' | 'LED' | 'LED_EXTENDED' }),头部奇偶校验、帧类型和数据长度必须一致。返回数字是传输成功后采样的主机时间,单位为从脚本启动起的微秒,不是设备 ACK 时间戳。

MeLiBu 调度表窗口列出计划的执行顺序。示例数据。
运行 MeLiBu 计划
await melibuStartPlan({ schedule: 'Main' });
// 或:await melibuStartPlan({ script: 'lighting.mbdfscr' });
await melibuStopPlan();MBDF 调度表或 .mbdfscr 使用调度窗口的同一套编译器、runtime 和信号状态。相对脚本路径以工程目录为基准。同一时刻只运行一个计划;脚本请求共用传输通道,在计划帧之间及空闲间隔中执行。最多等待 64 条请求,队列满时 reject melibu.queueFull。循环计划运行时,比循环中每个间隔都长的请求以 melibu.noGap reject,不会一直等待。停止脚本会以 melibu.cancelled 清掉该脚本的待执行请求,并停止它启动的计划;停止测量也会关闭共享 runtime。
启动测量后,melibuSend、melibuRead、原始 output 和计划共用正式 USB 设备通道。上位机先从 CAN CDC 查询固件能力,确认协议版本和速率后再初始化;无需占用 Debug 端口或安装 Python 辅助程序。未初始化或旧固件没有该能力时返回 melibu.unsupported。固件通道实现 1 Mbit/s、2 Mbit/s 与 MeLiBu 1.0/1.1,实际速率以初始化确认结果为准。
TX 时间戳来自 MCU 开始产生 BREAK 前的时钟读数(deviceTxStart),不等于硬件捕获的总线边沿;RX 时间戳仍未知。独立被动监听及设备侧计划未开放。主机事务有统一截止时间,循环计划只在足够容纳完整事务的间隔内插入脚本请求;不保证 1 ms 周期。正式通道尚待实物验收;保留 9.4.6 的设备不会自动升级,也不会自动退回 Debug 兼容通道。
定时器和回调中应捕获收发错误,避免 Worker 因未处理异常退出。示例 melibu.MBDF 与 melibu.ts 演示事件处理、定时发送和按 r 读取。唤醒、自动寻址及 E2E 脚本接口不在本期。
