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 messageevent and thethiskeyword - 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 startis 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
thismeans 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 inon message *causes recursive sending, because each sent message triggers anotheron messageevent.
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:
messageis 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 secondsmsTimer- 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:
- One second after the program starts, the first
on timeris triggered - The heartbeat message is sent
- The timer is reset, and after another second it triggers again
- 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:
-
Create a new database in the Database Manager
-
Add a message:
- Name:
EngineData - ID:
0x100 - DLC:
8bytes
- Name:
-
Add two signals:
EngineSpeed: 16 bits long, position 0-15EngineTemp: 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:
- Receive the
EngineDatamessage from the engine (ID=0x100, containing RPM and temperature) - When RPM exceeds 6000 RPM, automatically send a
WarningLightmessage (ID=0x200) to turn on the warning light - When temperature exceeds 110 degrees Celsius, send a
WarningLightmessage and log it - 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 messageand thethiskeyword - 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
EngineDataandWarningLightmessages.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 ofmessage EngineData)- Data access:
this.word(0) / 4(instead ofthis.EngineSpeed)- Warning setting:
warningMsg.byte(0) = 1(instead ofwarningMsg.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 startfor initialization,on preStopfor cleanup - Message reception: The
on messageevent and thethiskeyword for accessing message content - Message selectors: Using
*,ID,name, orchannelto filter messages - Message transmission: The
output()function is CAPL's "voice" for talking to the bus - Timers:
setTimer()+on timerfor 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
-
Basic Exercise: Write a program that receives all messages and prints their ID, data length, and the first 4 bytes of data.
-
Timer Exercise: Create an
msTimerthat sends an incrementing counter every 500 milliseconds. -
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
IdleStatusmessage. -
Advanced Exercise (with DBC environment): Create a DBC containing a
DoorStatusmessage (with aDoorOpensignal), and write a CAPL program that, whenDoorOpen=1is received, automatically sends anAlarmmessage 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.