CAPL Script

Core Interaction with Bus

Learning Objectives

After completing this article, you will be able to:

  • Understand the CAPL program lifecycle and the purpose of each stage
  • Master the on message event and the this keyword
  • Use the output() function to send CAN messages
  • Understand message selectors and symbolic programming
  • Write complete response logic: automatically send response messages when specific signals are received

In Article 3, we learned the syntax fundamentals of CAPL. Now, it's time to make your CAPL program truly come alive.

Imagine this: you've just written your first on key 'a' event handler, pressed the 'a' key, and "Hello CAPL!" appeared in the Write window. But then you might wonder: How does my program communicate with the CAN bus?

That's exactly what this article addresses. We'll learn the "language" CAPL uses to communicate with the bus world - the event-driven mechanism.


The CAPL Program Lifecycle

In the CAPL world, programs don't start from a main() function - they're event-driven. Nevertheless, CAPL programs have their own lifecycle.

Tip: Just like a person goes through different stages from birth to death, a CAPL program has key moments from startup to shutdown.

Four Key Lifecycle Events

The CAPL program lifecycle is controlled by four core events:

Event Trigger Timing Typical Use
on preStart Measurement initialization phase (before start) Early initialization, logging start (cannot use output())
on start Program startup moment Variable initialization, configuration setup, starting timers
on preStop When stop measurement is requested Resource cleanup, state saving, sending shutdown messages
on stopMeasurement When measurement ends Final cleanup, report generation

Practical Example

Let's look at a typical lifecycle program:

variables {
    msTimer heartbeatTimer;  // Timer variable declaration
}

on preStart
{
    write("=== Program starting: prepare environment ===");
}

on start
{
    write("=== Program started: initialize variables ===");

    int engineSpeed = 0;
    message 0x100 EngineDataMsg;

    // Set periodic timer
    setTimer(heartbeatTimer, 1000);  // Send heartbeat every second
}

on preStop
{
    write("=== Stop requested: cleanup resources ===");
    cancelTimer(heartbeatTimer);  // Cancel timer
}

on stopMeasurement
{
    write("=== Program ended: generate final report ===");
}

Note: on start is the most commonly used lifecycle event. This is typically where you initialize variables, set up timers, and more.


Event-Driven Model (Part 1): Receiving and Responding

CAPL is an event-driven language. This means the program stays in a "dormant" state most of the time and only "wakes up" to execute code when specific events occur.

The most essential event is on message - triggered when a CAN message is received.

on message Basics

on message *
{
    // * means receive all messages
    write("收到消息:ID=0x%X", this.ID);
}

This code means: No matter what message is received, print its ID.

Using the this Keyword to Access Message Content

In an on message event, the this keyword represents the message that was just received. You can access all properties of the message through this:

on message *
{
    // Access basic message properties
    write("Message ID: 0x%X", this.ID);        // Message identifier
    write("Data length: %d", this.DLC);        // Data length code
    write("Channel: %d", this.CAN);            // CAN channel number

    // Access message data (byte by byte)
    write("Data: %02X %02X %02X %02X",
          this.byte(0), this.byte(1),
          this.byte(2), this.byte(3));

    // Use DIR selector to check if received or sent
    // dir can be 'rx' (received) or 'tx' (transmitted)
    if (this.dir == rx) {
        write("This is a received message");
    }
}

Note: Understanding what this means is crucial - it points to the "subject" of the current event, which is the message object just received.

Message Selectors: Precise Message Filtering

If you only want to handle specific messages, you can use message selectors:

Method 1: Filter by ID

// Process only messages with ID 0x100
on message 0x100
{
    write("Received EngineData message");
}

Method 2: Filter by Name (requires DBC database)

// If EngineData message (ID=0x100) is defined in database
on message EngineData
{
    // Directly access signals using signal names
    write("Engine Speed: %d RPM", this.EngineSpeed);
    write("Engine Temp: %d C", this.EngineTemp);
}

Method 3: Filter by Channel

// Process only messages from CAN1 channel
on message CAN1.*
{
    write("Message from CAN1 channel");
}

Tip: Start simple (use on message * to observe all messages), then gradually refine your filter conditions.

Hands-On: Message Reception Monitor

Let's write a complete message monitor:

variables {
    // Statistics counters
    int totalMessages = 0;
    int engineDataCount = 0;
}

on message *
{
    totalMessages++;
    write("[%d] Received message: 0x%X", totalMessages, this.ID);
}

on message 0x100  // EngineData message
{
    engineDataCount++;
    write("  -> This is EngineData message (#%d)", engineDataCount);

    // Check data
    if (this.byte(0) > 0x80) {
        write("  -> Warning: Engine speed too high!");
    }
}

Run this program, and you'll see all message traffic, including detailed analysis of specific messages.

[!SCREENSHOT]
Location: CAPL Browser > Write Window
Content: Showing message reception log
Annotation: Circle the "Received message" and "EngineData message" output lines


Event-Driven Model (Part 2): Sending and Control

Now that we've learned to "listen" (receive), let's learn to "speak" (send).

The output() Function: Sending Messages

output() is the core function for sending messages in CAPL:

void output(message msg);

Important Note: Avoid Recursive Sending

// ❌ Wrong: do not use output(this) in on message
on message * {
    output(this);  // This causes infinite recursion!
}

// ✅ Correct: check message direction
on message 0x100 {
    if (this.dir == rx) {  // Only process received messages
        // Process message without resending
    }
}

Note: Using output(this) directly in on message * causes recursive sending, because each sent message triggers another on message event.

Basic Send Example

on key 's'  // Press 's' to send message
{
    // Step 1: Declare message variable
    message 0x200 HeartbeatMsg;

    // Step 2: Set message data
    HeartbeatMsg.byte(0) = 0xAA;  // Heartbeat signature
    HeartbeatMsg.byte(1) = 0x55;  // Fixed data

    // Step 3: Send message
    output(HeartbeatMsg);

    write("Heartbeat message sent");
}

Press the 's' key once, and a message is sent to the CAN bus.

Note: message is a special data type in CAPL used to represent CAN frames.

Timer Events: Implementing Periodic Transmission

output() can only send a message once. To implement periodic transmission (such as sending a heartbeat every second), we need timers.

Timer Type Comparison

Feature timer msTimer
Time Unit Seconds Milliseconds
Maximum Time 596.52 hours 596.52 hours
Common Use Case Long-duration timing Fast response tasks
Syntax timer myTimer; msTimer myTimer;

CAPL supports two types of timers:

  • timer - based on seconds
  • msTimer - based on milliseconds (more commonly used)

Complete Periodic Transmission Example

variables {
    msTimer heartbeatTimer;  // Declare millisecond timer
    message 0x200 HeartbeatMsg;
    int heartbeatCount = 0;
}

on start
{
    write("Starting periodic heartbeat transmission...");
    setTimer(heartbeatTimer, 1000);  // Trigger first time after 1 second
}

on timer heartbeatTimer
{
    heartbeatCount++;

    // Prepare heartbeat message
    HeartbeatMsg.byte(0) = 0xAA;      // Heartbeat signature
    HeartbeatMsg.byte(1) = heartbeatCount & 0xFF;  // Counter

    // Send message
    output(HeartbeatMsg);

    write("Heartbeat #%d sent", heartbeatCount);

    // Re-arm timer to create loop
    setTimer(heartbeatTimer, 1000);
}

This code produces the following effect:

  1. One second after the program starts, the first on timer is triggered
  2. The heartbeat message is sent
  3. The timer is reset, and after another second it triggers again
  4. This cycle continues, creating a heartbeat every second

Tip: A timer is like an alarm clock - after it goes off, you need to reset it for it to ring again.

Canceling a Timer

on key 'x'  // Press 'x' key to stop heartbeat
{
    cancelTimer(heartbeatTimer);
    write("Heartbeat stopped");
}

Combined Usage: Responsive Sending

The most practical pattern is: Receive message -> Process logic -> Send response.

variables {
    message 0x100 EngineData;   // Received message
    message 0x200 WarningLight; // Response message
}

on message 0x100  // Receive EngineData
{
    // Read engine speed value (assume it's in the first two bytes)
    int engineSpeed = this.byte(0) * 256 + this.byte(1);

    write("Received engine speed: %d RPM", engineSpeed);

    // Logic check: engine speed too high
    if (engineSpeed > 6000) {
        write("Engine speed too high! Sending warning light signal");

        // Prepare warning message
        WarningLight.byte(0) = 0x01;  // Turn on warning light

        // Send response message
        output(WarningLight);
    }
}

Note: This is a typical reactive system - it produces output (messages) based on input (messages), which is the core logic of CAPL simulation nodes.


Database (DBC) Integration - From "Numbers" to "Semantics"

Up to this point, we've been using numbers (like 0x100) and bytes (like this.byte(0)) to manipulate messages. While this is the low-level approach, it's not very intuitive.

DBC databases allow us to program using semantic names.

DBC Database Terminology

Term Full Name Description
DBC Database CAN CAN database file format
Message Message CAN frame containing ID and data
Signal Signal Specific data field within a message
Encoding Encoding How signal data is represented (raw value/physical value)

Why Do We Need DBC?

Without a database:

on message 0x100
{
    // What do these numbers mean?
    int rpm = this.byte(0) * 256 + this.byte(1);
    int temp = this.byte(2);
}

With a database:

on message EngineData
{
    // Directly use meaningful names
    write("Engine Speed: %d", this.EngineSpeed);
    write("Engine Temp: %d", this.EngineTemp);
}

Which is clearer? Obviously the second one.

Creating a Simple DBC in CANoe

[!SCREENSHOT]
Location: CANoe > Database Manager
Content: Showing the interface for creating a new database
Annotation: Circle the "Create New Database" button

Let's create the simplest DBC:

  1. Create a new database in the Database Manager

  2. Add a message:

    • Name: EngineData
    • ID: 0x100
    • DLC: 8 bytes
  3. Add two signals:

    • EngineSpeed: 16 bits long, position 0-15
    • EngineTemp: 8 bits long, position 16-23

Note: The DBC file defines the structure of messages and signals (name, ID, length, position, etc.), allowing CAPL to perform symbolic programming.

Symbolic Programming in Practice

With a DBC, CAPL code becomes intuitive and easy to understand:

on message EngineData  // Use message name directly
{
    // Use signal names directly, no need to parse bytes manually
    int rpm = this.EngineSpeed;
    int temp = this.EngineTemp;

    write("Engine speed: %d RPM", rpm);
    write("Engine temp: %d C", temp);

    // Logic check
    if (temp > 110) {
        write("Engine overheating!");

        // Send warning message (also use symbolic name)
        WarningLight.LampStatus = 1;
        output(WarningLight);
    }
}

The getMessageID() Function: Dynamically Retrieving Message IDs

Sometimes you need to dynamically get an ID from a message name:

on start
{
    dword engineDataId;

    // Get message ID from database
    engineDataId = getMessageID("EngineData");

    if (engineDataId != -1) {
        write("EngineData message ID is: 0x%X", engineDataId);
    } else {
        write("Error: EngineData message not found");
    }
}

Tip: In multi-database environments, you need to specify the database name: getMessageID("DatabaseName.EngineData")

This is useful when you need to handle messages dynamically.

Note: Symbolic programming is an advanced CAPL skill - it makes code more readable and maintainable. We recommend always using DBC databases for development.


Comprehensive Case Study: Smart Engine Speed Monitor Node

Now let's integrate all the concepts we've learned and write a complete smart monitoring node.

Note: This case uses raw programming (no DBC database required), using direct CAN IDs and byte access. This is the best way to understand how CAPL works at a low level.

Requirements Definition

We'll write an ECU simulation node with the following functionality:

  1. Receive the EngineData message from the engine (ID=0x100, containing RPM and temperature)
  2. When RPM exceeds 6000 RPM, automatically send a WarningLight message (ID=0x200) to turn on the warning light
  3. When temperature exceeds 110 degrees Celsius, send a WarningLight message and log it
  4. Send a heartbeat message every second

Step 1: Define Variables

variables {
    // Message variables (using raw CAN IDs)
    message 0x100 engineMsg;      // Received message
    message 0x200 warningMsg;     // Sent message
    message 0x200 heartbeatMsg;   // Heartbeat message (reuses same ID)

    // Timer
    msTimer heartbeatTimer;

    // Statistics variables
    int totalEngineMsgs = 0;
    int warningTriggered = 0;
    int heartbeatCount = 0;

    // Constants for thresholds
    const int RPM_THRESHOLD = 6000;
    const int TEMP_THRESHOLD = 110;
    const int TEMP_OFFSET = 40;  // Temperature offset: raw + 40 = actual degC
}

Step 2: Initialization

on start
{
    write("=== Smart Engine Monitor Node Started (Raw Mode) ===");
    write("Using raw CAN IDs - no DBC database required");

    // Initialize heartbeat message
    heartbeatMsg.dlc = 1;
    heartbeatMsg.byte(0) = 0xAA;  // Heartbeat signature

    // Start periodic heartbeat
    setTimer(heartbeatTimer, 1000);

    write("Initialization complete");
}

Step 3: Receive Messages and Process

on message 0x100  // EngineData message
{
    // Local variables for extracted values
    int rawRpm;
    int actualRpm;
    int rawTemp;
    int actualTemp;

    totalEngineMsgs++;

    // Extract RPM value (bytes 0-1, 16-bit, scale 0.25)
    rawRpm = this.word(0);
    actualRpm = rawRpm / 4;

    // Extract Temperature value (byte 2, offset -40)
    rawTemp = this.byte(2);
    actualTemp = rawTemp - TEMP_OFFSET;

    write("[%d] Received EngineData (0x100): RPM=%d (raw=%d), Temp=%d degC (raw=%d)",
          totalEngineMsgs, actualRpm, rawRpm, actualTemp, rawTemp);

    // Logic check 1: Engine speed too high
    if (actualRpm > RPM_THRESHOLD) {
        warningTriggered++;
        write("*** WARNING #%d: Engine speed too high (%d RPM)! ***",
              warningTriggered, actualRpm);

        // Send warning light message
        warningMsg.dlc = 1;
        warningMsg.byte(0) = 1;  // LampStatus = ON
        output(warningMsg);

        write("WarningLight (0x200) sent: LampStatus=1 (ON)");
    }

    // Logic check 2: Engine temperature too high
    if (actualTemp > TEMP_THRESHOLD) {
        warningTriggered++;
        write("*** WARNING #%d: Engine temperature too high (%d degC)! ***",
              warningTriggered, actualTemp);

        // Send warning light message
        warningMsg.dlc = 1;
        warningMsg.byte(0) = 1;  // LampStatus = ON
        output(warningMsg);

        write("WarningLight (0x200) sent: LampStatus=1 (ON)");
    }

    // Normal state
    if (actualRpm <= RPM_THRESHOLD && actualTemp <= TEMP_THRESHOLD) {
        write("Status: Engine operating normally");
    }
}

Step 4: Periodic Heartbeat

on timer heartbeatTimer
{
    heartbeatCount++;

    // Update heartbeat data
    heartbeatMsg.dlc = 1;
    heartbeatMsg.byte(0) = heartbeatCount & 0xFF;

    // Send heartbeat message
    output(heartbeatMsg);

    write("Heartbeat #%d sent on 0x200 (data: 0x%02X)",
          heartbeatCount, heartbeatCount & 0xFF);

    // Re-arm timer
    setTimer(heartbeatTimer, 1000);
}

Step 5: Resource Cleanup

Note: All comments in CAPL code should be in English to avoid character encoding issues.

on preStop
{
    write("=== Stopping monitor node ===");
    cancelTimer(heartbeatTimer);

    write("Total engine messages processed: %d", totalEngineMsgs);
    write("Total warnings triggered: %d", warningTriggered);
}

on stopMeasurement
{
    write("=== Monitor node stopped ===");
}

Complete Code Overview

variables {
    message 0x100 engineMsg;
    message 0x200 warningMsg;
    message 0x200 heartbeatMsg;
    msTimer heartbeatTimer;
    int totalEngineMsgs = 0;
    int warningTriggered = 0;
    int heartbeatCount = 0;
    const int RPM_THRESHOLD = 6000;
    const int TEMP_THRESHOLD = 110;
    const int TEMP_OFFSET = 40;
}

on start
{
    write("=== Smart Engine Monitor Node Started (Raw Mode) ===");
    heartbeatMsg.dlc = 1;
    heartbeatMsg.byte(0) = 0xAA;
    setTimer(heartbeatTimer, 1000);
    write("Initialization complete");
}

on message 0x100
{
    int rawRpm = this.word(0);
    int actualRpm = rawRpm / 4;
    int rawTemp = this.byte(2);
    int actualTemp = rawTemp - TEMP_OFFSET;

    totalEngineMsgs++;
    write("[%d] Received EngineData: RPM=%d, Temp=%d degC",
          totalEngineMsgs, actualRpm, actualTemp);

    if (actualRpm > RPM_THRESHOLD) {
        warningTriggered++;
        write("Warning #%d: Engine speed too high (%d RPM)!", warningTriggered, actualRpm);
        warningMsg.dlc = 1;
        warningMsg.byte(0) = 1;
        output(warningMsg);
    }

    if (actualTemp > TEMP_THRESHOLD) {
        warningTriggered++;
        write("Warning #%d: Engine temperature too high (%d C)!", warningTriggered, actualTemp);
        warningMsg.dlc = 1;
        warningMsg.byte(0) = 1;
        output(warningMsg);
    }

    if (actualRpm <= RPM_THRESHOLD && actualTemp <= TEMP_THRESHOLD) {
        write("Status: Engine normal");
    }
}

on timer heartbeatTimer
{
    heartbeatCount++;
    heartbeatMsg.dlc = 1;
    heartbeatMsg.byte(0) = heartbeatCount & 0xFF;
    output(heartbeatMsg);
    write("Heartbeat #%d", heartbeatCount);
    setTimer(heartbeatTimer, 1000);
}

on preStop
{
    write("=== Stopping monitor node ===");
    cancelTimer(heartbeatTimer);
    write("Total engine messages processed: %d", totalEngineMsgs);
    write("Total warnings triggered: %d", warningTriggered);
}

on stopMeasurement
{
    write("=== Monitor node stopped ===");
}

This program integrates all the core concepts we've learned:

  • Lifecycle events: Initialization and cleanup
  • Message reception: on message and the this keyword
  • Message transmission: The output() function
  • Periodic tasks: Timers and on timer
  • Symbolic programming: Using message and signal names from DBC
  • Business logic: Conditional checks and status monitoring

Note: This example uses symbolic programming (based on a DBC database), which is the recommended approach for real projects. Before using symbolic programming, you need to create a DBC database in CANoe containing the EngineData and WarningLight messages.

If your environment doesn't have a DBC database, you can refer to the raw programming version (no database required):

  • Message declaration: message 0x100 engineMsg (instead of message EngineData)
  • Data access: this.word(0) / 4 (instead of this.EngineSpeed)
  • Warning setting: warningMsg.byte(0) = 1 (instead of warningMsg.LampStatus = 1)

Both programming approaches are functionally identical, just different in style. Symbolic programming is more readable, while raw programming is more self-contained.

Tip: Try modifying this program - for example, adjust the RPM/temperature thresholds, add new warning conditions, or implement additional monitoring features.

[!SCREENSHOT]
Location: CANoe > Measurement Setup > Write Window
Content: Showing the smart monitoring node's runtime log
Annotation: Circle the "Received EngineData", "Warning", and "Heartbeat" output lines


Summary

In the CAPL world, event-driven programming is the core mechanism. We've learned:

  • Lifecycle management: on start for initialization, on preStop for cleanup
  • Message reception: The on message event and the this keyword for accessing message content
  • Message selectors: Using *, ID, name, or channel to filter messages
  • Message transmission: The output() function is CAPL's "voice" for talking to the bus
  • Timers: setTimer() + on timer for implementing periodic tasks
  • Symbolic programming: DBC databases transform code from "numbers" to "semantics"

These are the foundations of CAPL programming - master them, and you'll be able to build complex ECU simulation nodes.

Exercises

  1. Basic Exercise: Write a program that receives all messages and prints their ID, data length, and the first 4 bytes of data.

  2. Timer Exercise: Create an msTimer that sends an incrementing counter every 500 milliseconds.

  3. Logic Exercise: Modify the comprehensive case study in this article by adding a new check: when RPM drops below 800 RPM, consider it an "idle state" and send an IdleStatus message.

  4. Advanced Exercise (with DBC environment): Create a DBC containing a DoorStatus message (with a DoorOpen signal), and write a CAPL program that, when DoorOpen=1 is received, automatically sends an Alarm message after a 2-second delay.


Preview: In the next article, we'll learn how to debug CAPL programs. Debugging is an essential skill for every programmer - we'll start with the simplest write() function and progressively learn about breakpoints, watch windows, and other powerful tools.