PC Scripts
Create and edit
After importing a DBC/LDF, use Manage nodes to assign the database nodes to simulate. Unassigned nodes do not send periodic frames, provide LIN responses, or run scripts. Removing a database node from the bus keeps its state, script, and position for reassignment. Existing projects retain database nodes with saved settings.
Add a custom node on the CAN / LIN canvas in Bus Config, or open Node settings for a database node. New nodes are on by default; change Status to turn them off. Bind one script per node with New… or Select…. Remove script clears the binding and keeps the file. Custom nodes must connect to CAN, LIN, or both.
Card actions open or add the script, open Node settings, and open Node Panel (database nodes only). Focus a card and press Space to toggle activation, Enter for settings, Delete to remove a custom node, or Shift+F10 for its menu. Space in the tree only selects a node. Inactive cards and wires use dashed borders. Compile in the menu compiles only that node and reports results in Write. Deletion requires confirmation and keeps the script file.
Paths are relative to the .blproj directory. Copy scripts and dependencies together with the project. Edit opens the system default application. Open the whole project folder in VS Code for type hints.
Adding scripts generates tsconfig.json and node_modules/@types/beinilink/index.d.ts, including variable names/types, CAN/LIN frame names and signal names. Variable or database configuration changes regenerate declarations. Existing tsconfig settings and comments are preserved; only files entries are appended. If you customize compilerOptions.types, include node and beinilink yourself.
Start, stop and errors
Starting measurement compiles and runs only scripts on active, placed nodes. Their syntax errors prevent startup; Write shows the node name and file:line:column. Inactive nodes are not compiled at startup; Write records Script: <node> and skipped (inactive). Compile in Node settings checks the selected file before OK and shows success or the error count; changing the path clears this result. Compilation transforms and bundles code without type checking. Measurement requires at least one open hardware port. During measurement, assignments are read only and existing scripts can still open in the editor. Activation is locked on scripted nodes; database nodes without scripts can still toggle periodic transmission.
Each node runs in a separate Node.js Worker. Multiple nodes can share one file; the read-only NODE_NAME identifies each instance. Nodes can only send, receive, and access signals on their connected buses. Variables and key events remain shared. Variables start before sequential Util.Init callbacks. Stop invokes Util.End, waits up to two seconds per node, then terminates the Workers. An uncaught exception stops only that node's script. Write receives logs/errors, and node cards show the script state. Legacy global scripts migrate to custom nodes connected to both CAN and LIN.
Run only scripts you trust. PC scripts have full Node.js file and network permissions. This feature is separate from the device Python REPL.
Send CAN from a slider
Keep the example script.blproj and script.ts together. Open the project, select your device, check the 500 kbit/s CAN configuration, open Lamp and Trace, then start measurement. Moving the slider sends ID 0x321 with Lamp.Level (0–100) in byte 0. Receiving 0x123 updates the variable and display from byte 0 and also triggers the variable's transmit callback.
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() resolves with host microseconds since script startup when the backend accepts the request. It is not a hardware ACK timestamp. Incoming ts uses an available device timestamp and is undefined when absent.
Signals and variables
Use Message.Signal names. For a collision between DBC and LDF, use CAN.Message.Signal or LIN.Frame.Signal. With Status.Speed in your database:
Util.OnSignal('Status.Speed', signal => console.log(signal.physValue));
console.log(getSignal('Status.Speed').rawValue);
Util.Init(async () => {
await setSignal('Status.Speed', 100); // Raw value
await setSignal('Status.Speed', '25'); // Physical value, or an enum label
});getSignal returns the latest value decoded by this Worker, or its database initial value before receiving a frame. OnSignal runs whenever the corresponding frame is decoded and provides raw and physical values. During measurement, setSignal follows the panel write path: it updates simulation data; node activation, message periods and LIN schedules govern transmission.
getVar reads synchronously. setVar / setVars validate and update the local mirror, then send changes to the shared variable runtime. Local validation failures throw an exception; a write rejected by the main process logs a warning in Write and the script keeps running. Use full variable names. System variables are read only. Util.OnVar('*', ({name, value}) => ...) observes all variables.
Events and LIN
OnCan / OnLin accept an ID, frame name or true (all). These APIs, OnSignal, OnVar and OnKey also have Once and Off variants. Unsubscribe with the same callback function you registered. Only subscribed bus frames are forwarded; a Worker that persistently falls behind exits with an error.
const onFrame = (msg: CanMessage) => console.log(msg.data.toString('hex'));
Util.OnCan(0x123, onFrame);
Util.OffCan(0x123, onFrame);
Util.OnKey('x', () => console.log('x'));Keys are forwarded during measurement, except in inputs, text editors, selects or dialogs, and excluding Esc. Node timers and console.log/warn/error are available.
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 scheduling requires an open master and a schedule in the project LDF. For requests use LinDirection.RECV; data length specifies the expected 1–8 bytes. The current device interface does not support RECV_AUTO_LEN. Power control requires supporting LIN hardware.
Imports from ECB and beinilink are supported, for example import { Util, output } from 'ECB'. UDS, SOME/IP, DoIP, test framework and serial script APIs are outside this phase.
Flash an ECU
The examples flash products with a built-in bootloader over CAN or LIN from a PC script. The whole flashing flow is implemented in the scripts, and both projects share flash-uds.ts:
- CAN: flash.blproj and flash.ts, classic CAN with ISO-TP.
- LIN: flash-lin.blproj and flash-lin.ts. The master sends on
0x3Cand polls responses on0x3Dwith classic checksum. Do not run a schedule at the same time.
Name your firmware firmware.bin and place it in the project folder, change config at the top of the script to match your ECU bootloader, check the bitrate, start measurement and press f. Progress appears in Write. Keep the script files in one folder.
| Step | Service | Description |
|---|---|---|
| 1 | 10 03, 10 02 | Enter the extended and programming sessions |
| 2 | 27 11, 27 12 | Encrypt the 16-byte seed with AES-128 ECB and send the result as the key |
| 3 | 31 01 FF00 | Erase, with address and length as parameters |
| 4 | 34, 36, 37 | Download using the block length returned by the ECU |
| 5 | 31 01 0202 | Send the CRC32 of the whole image. A status byte of 0 from the ECU means it passed |
| 6 | 11 01 | Reset the ECU |
Any failed step stops the flash and Write shows the negative response code. The ECU is not reset after a failed check. Service IDs, routine IDs and parameter formats must match your bootloader. The AES key in the example is for demonstration only.
Listen to MeLiBu
Import an MBDF, assign a node to the MeLiBu bus, then bind and activate its script in Node settings. These nodes use the same New, Select and Compile actions as CAN/LIN nodes. Generated declarations include MelibuFrameName, MelibuNodeName and MBDF signal names. Only nodes connected to MeLiBu can use these APIs.
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 accept an MBDF frame name, a raw 16-bit ID or true. data is a Buffer; host transmissions are OUT, slave responses are IN. Read headers and responses are separate events. Ambiguous matches provide candidates without choosing a frame. CRC errors and captured partial data remain observable. ts is absent when the capture has no known timestamp. ACK-only events are kept in Trace rather than delivered as data frames.
Use MELIBU.Voltage for the full signal name. The short name Voltage is available when unique across the signal catalog. CAN/LIN full names retain CAN.Message.Signal and LIN.Frame.Signal. getSignal reads a synchronous mirror of the shared native runtime; OnSignal follows native updates from plans, script transfers and node-window edits. Raw MBDF values and initial values are decimal strings, including all 64 bits. physValue is present for a valid physical encoding; updatedFrom identifies the source MBDF frame when available.
Send and read 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);Signal values are keyed by name. Omitted values use the shared current state when the request is dispatched. Safe integer numbers and unsigned decimal integer strings specify raw values. Other numeric strings specify physical values; use '25.0' for an integer physical value. Logical encoding labels are also accepted. setSignal uses the same rules for MBDF signals; CAN/LIN string conversion remains as described above.
melibuSend requires an M2S frame. Targets may be an MBDF node or Broadcast / Broadcast0–Broadcast3. An optional fourth argument overrides { subAddress, instruction }; fixed header fields cannot change. melibuRead requires an S2M frame and a unique node. A valid response updates shared signals before the returned Promise resolves. CRC failures, timeouts and transport failures reject with melibu.crc, melibu.timeout and melibu.transport; failed reads keep previous signal values. ACK failures use melibu.noAck or melibu.nack.
For raw M2S output, use output({ rawId, data: Buffer, kind: 'DATA' | 'LED' | 'LED_EXTENDED' }). Header parity, kind and payload length must agree. The returned number is host microseconds since script startup, sampled after the transfer succeeds; it is not a device ACK timestamp.

The MeLiBu schedule window lists the plan's execution order. Sample data.
Run a MeLiBu plan
await melibuStartPlan({ schedule: 'Main' });
// Or: await melibuStartPlan({ script: 'lighting.mbdfscr' });
await melibuStopPlan();The MBDF schedule or .mbdfscr uses the same compiler, runtime and signal store as the MeLiBu scheduler window. Relative script paths resolve against the project folder. Only one plan runs at a time. Script transfers share its transport and run between plan frames, including idle gaps. Up to 64 waiting requests are allowed; a full queue rejects with melibu.queueFull. While a looping plan runs, a request that is longer than every gap of the loop rejects with melibu.noGap instead of waiting forever. Stopping a script cancels its waiting requests with melibu.cancelled and stops a plan it started. Stopping measurement also closes the shared runtime.
After measurement starts, melibuSend, melibuRead, raw output and plans share the native USB device channel. The host queries capabilities on CAN CDC before initializing the requested protocol and rate. Debug CDC and a Python helper are not required. Calls before initialization, or firmware without this capability, reject with melibu.unsupported. The firmware implements 1 Mbit/s and 2 Mbit/s with MeLiBu 1.0/1.1; the open response confirms the configured rate.
TX timestamps use the MCU clock immediately before generating BREAK (deviceTxStart), not hardware capture of the bus edge. RX timestamps remain unknown. Passive monitoring and device-side plans are not enabled. Host transactions share one deadline, and scripts enter a looping plan only when a gap can accommodate the full transaction budget. A 1 ms period is not guaranteed. Hardware acceptance is pending; devices kept on 9.4.6 are neither upgraded automatically nor switched to a Debug compatibility path.
Catch transfer errors in timers and callbacks so the Worker can continue. The example files melibu.MBDF and melibu.ts demonstrate event handling, periodic sending and reading with the r key. Wake-up, auto-addressing and E2E script APIs are outside this phase.
