CAPL Script

How to Develop a Simulation Node

Define the model before implementing events

This door-lock and window model connects receive events, state changes, timers and status messages. The protocol and acceptance procedure are teaching examples, not a production door protocol, anti-pinch implementation or functional-safety design. The source has not been compiled or executed in CANoe here. Validate it against the installed help in an isolated simulation configuration.

Use a Simulation Setup node, with CAN1 mapped to the intended channel, standard identifiers and classical CAN data frames only. Disable other senders using these IDs and exclude CAN FD and extended frames. No DBC is required; if a database is present, check for ID conflicts first. This is not a transparent forwarding node for a measurement evaluation branch.

A message and state contract you can inspect

CAN1 standard ID Direction and payload Rule in this example
0x200 Receive lock command; payload ignored Lock while stationary; reject while moving
0x201 Receive unlock command; payload ignored Unlock while stationary; reject while moving
0x202 Receive window command; DLC exactly 1 byte 0 = 1: down; 2: up; reject other values
0x300 Request transmission when the status timer expires; DLC 3 byte 0: lock; byte 1: position; byte 2: movement

Lock state 0 means unlocked and 1 means locked. Position 0 is fully up; 100 is fully down. Movement 0 is stationary, 1 is up and 2 is down. Interlocks are arbitrary rules of this example: reject locking/unlocking during motion and reject window commands while locked. They are not safety requirements for a real vehicle.

Measurement starts unlocked, stationary and at position 0. One accepted window command starts movement. Commands received during motion are rejected rather than queued or used to reverse direction. A request for the current endpoint is a no-op and starts no timer.

One source program to inspect

Use this as one complete teaching source, without adding duplicate event procedures. Receive commands do not need separate message variables. Read the payload of the current window event through this.

variables
{
  byte doorLockState;
  long windowPosition;
  byte windowMovement;
  msTimer windowTimer;
  msTimer statusTimer;
  message CAN1.0x300 doorStatus;
}

on start
{
  cancelTimer(windowTimer);
  cancelTimer(statusTimer);
  doorLockState = 0;
  windowPosition = 0;
  windowMovement = 0;
  doorStatus.RTR = 0;
  doorStatus.DLC = 3;
  setTimer(statusTimer, 500);
  write("DoorModule initialized: unlocked, position 0");
}

on message CAN1.0x200
{
  if (this.DIR != Rx || this.RTR != 0) return;
  if (windowMovement != 0)
  {
    write("Lock rejected: window moving");
    return;
  }
  doorLockState = 1;
  write("Lock accepted");
}

on message CAN1.0x201
{
  if (this.DIR != Rx || this.RTR != 0) return;
  if (windowMovement != 0)
  {
    write("Unlock rejected: window moving");
    return;
  }
  doorLockState = 0;
  write("Unlock accepted");
}

on message CAN1.0x202
{
  byte command;
  if (this.DIR != Rx || this.RTR != 0) return;
  if (this.DLC != 1)
  {
    write("Window rejected: expected exactly one byte");
    return;
  }
  command = this.byte(0);
  if (command != 1 && command != 2)
  {
    write("Window rejected: invalid command %d", command);
    return;
  }
  if (doorLockState != 0 || windowMovement != 0)
  {
    write("Window rejected: locked or already moving");
    return;
  }
  if ((command == 1 && windowPosition == 100) ||
      (command == 2 && windowPosition == 0))
  {
    write("Window unchanged: already at requested endpoint");
    return;
  }
  if (command == 1) windowMovement = 2;
  else windowMovement = 1;
  setTimer(windowTimer, 20);
  write("Window accepted: movement state %d", windowMovement);
}

on timer windowTimer
{
  if (windowMovement == 1)
  {
    if (windowPosition <= 5) windowPosition = 0;
    else windowPosition = windowPosition - 5;
  }
  else if (windowMovement == 2)
  {
    if (windowPosition >= 95) windowPosition = 100;
    else windowPosition = windowPosition + 5;
  }
  else return;

  if (windowPosition == 0 || windowPosition == 100)
  {
    windowMovement = 0;
    write("Window stopped at %d", windowPosition);
  }
  else setTimer(windowTimer, 20);
}

on timer statusTimer
{
  doorStatus.byte(0) = doorLockState;
  doorStatus.byte(1) = windowPosition;
  doorStatus.byte(2) = windowMovement;
  output(doorStatus);
  setTimer(statusTimer, 500);
}

Four ways a plausible result can be wrong

1. A message variable is not the current received frame

Declaring a variable called WindowCmd does not mean that it automatically becomes the received frame. Use this inside on message, and validate DLC before reading byte(0). A zero-length event arrived but contains no command byte: record a rejected payload rather than a missing frame.

2. Guard the boundary before arithmetic

Subtracting 5 from an unsigned byte holding 0 and only then testing for a value at or below zero can wrap on assignment. An endpoint command exposed this defect in the previous example. This version stores the internal position in a long, clamps values at or below 5 before subtraction, and clamps values at or above 95 before addition. Position stays within 0–100 before it is assigned to a status byte.

3. Nominal travel is 400 ms, not two seconds

Moving from 0 to 100 in increments of 5 requires 20 timer events. Rearming for 20 ms each time gives 20 × 20 = 400 ms nominal travel. This is a calculation from model parameters, not a measured timing guarantee. Scheduling delay and relative rearming affect actual timing; preserve event timestamps instead of treating the display time of a Write line as bus time.

The status timer is rearmed for 500 ms, longer than nominal full travel. A down command soon after initialization can therefore finish before the first status message. That message may already show position 100 and stationary, with no moving state recorded. Inspect internal state in the debugger, or shorten the status period in a separate, documented experiment. A missing intermediate status does not prove that motion never occurred.

4. A send request is not receiver confirmation

output provides no success return code. This example specifies CAN1, ID, RTR and DLC and writes all three bytes before each request. Still inspect Trace and the receiving observer separately. An “accepted” Write line describes this node's internal decision; it does not prove that an external ECU acted on the request.

Acceptance checks in an isolated configuration

Compile this single CAPL node first and resolve version or syntax issues reported by the installed compiler. Inject classical CAN data frames through a separate sender or Interactive Generator; UI names and positions vary by version. Lock and unlock trials can use DLC 0; window commands use DLC 1. Confirm that commands arrive as receive events at this node. Preserve both commands and 0x300 status in Trace.

These are expected checks, not recorded test results. Restart measurement for each row unless the row explicitly describes a sequence.

Input or operation Expected internal behavior Evidence to inspect
No commands Unlocked, position 0, stationary Status payload 00 00 00; CAN1, standard ID, DLC 3
At initial position 0, send 0x202 / 02 Remain at 0, stationary Endpoint no-op log; never a wrapped value such as 251
Send 0x202 / 01 Add 5 per movement event; stop at 100 Internal event order and times; final byte 1 is decimal 100, hex 0x64
At 100, send 01 again Remain at 100, stationary No new movement starts
At 100, send 02 Subtract 5 per event; stop at 0 Monotonic decrease without a negative or wrapped value
While moving, send reverse or lock command Reject; preserve current movement Rejection log and state; do not rely only on the next periodic status
While stationary, send 0x200, then 0x202 / 01 Lock; reject window command Confirm lock state and unchanged position; send 0x201 before controlling the window again
Send 0x202 with DLC 0 or 2 Reject No missing-byte read and no state change
Send 0x202 byte 0 = 00 or 03 Reject No movement timer starts
Send the same numeric ID on CAN2, or send RTR Not accepted as a command Confirm channel and frame kind; distinguish filtering from lost reception
Stop and restart measurement Reset to initial state No position or movement carried over from the prior run

Keep the CANoe version, channel mapping, sender identity, trace rows, internal state and rejection reasons. If a check fails, use the single-stream selection procedure to establish that the input reached the observed node before changing the model. Adding output(this) to forward everything can change the bus behavior you are trying to inspect.

Extend with repeatable faults first

For a resistance-stop extension, define the input, trigger position, resulting state and restart conditions first. Inject resistance on a fixed event, such as the seventh movement event, to inspect one repeatable path. Rejecting new commands does not detect an obstruction; the current model is not anti-pinch protection.

The former random() < 0.1 example did not match the CAPL reference used here. The signature is dword random(dword x), returning an integer from 0 to x-1. Even random(10) == 0 cannot promise exactly one event in every ten trials and makes a failure harder to reproduce. Consider randomized scenarios only after deterministic acceptance, preserving the generated input sequence.

To inspect the 0x300 period, export one stream using the timestamp observation workflow, then use the interval analyzer. Those statistics describe the selected status stream, not the correctness of window movement or a safety function.

API behavior follows the authorized Vector reference. The state contract, counterexamples and acceptance matrix are this site's analysis. See sources and validation limits.