05 · CAPL Scripting Basics¶
CAPL (Communication Access Programming Language) is Vector CANoe's built-in scripting language: C-like syntax, event-driven execution, and first-class support for CAN/LIN/Ethernet messages as native types. This module covers the real, documented core of the language — event handlers, message send/receive, and timers — the building blocks every later module (test modules, restbus simulation, fault injection) is built from.
About this module
The syntax and event-handler names below (on start, on message,
on timer, output(), setTimer(), message struct access) are
genuine, documented CAPL conventions. This course cannot compile or
execute CAPL against a live CANoe instance — every snippet here is a
faithful, reviewed example to study, not a transcript of an actual
run.
CAPL's execution model: event handlers, not a single main()¶
Unlike a normal C program with one main(), a CAPL program is a
collection of event procedures — blocks that CANoe's runtime invokes
when a specific event occurs. The three you'll use constantly:
variables
{
// Global variables live here, declared once for the whole program.
msTimer commandTimeout;
byte aliveCounter;
}
on start
{
// Runs once when the measurement (CANoe's term for a test run) begins.
write("Node simulation started.");
}
on message SensorNodeStatus
{
// Runs every time a frame matching this message is received.
write("Coolant temp raw = %d", this.CoolantTemp);
}
on timer commandTimeout
{
// Runs when the named timer expires.
write("Command timed out - entering failsafe.");
}
on start— fires once at the beginning of a measurement. Typical use: initialize variables, arm timers, send an initial state message.on message <Name or ID>— fires every time a frame matching that message (by symbolic name from the loaded database, or by raw ID) arrives on the bus.thisrefers to the message that triggered the handler.on timer <name>— fires once when amsTimer(millisecond-resolution timer) you started withsetTimer()expires.
Other common handlers: on key (keyboard input, useful for manual test
panels), on preStart/on stopMeasurement (setup/teardown around a
measurement), and on signal (fires on a signal change rather than a
raw message — useful once a database is loaded).
Sending a message¶
CAPL represents each CAN message from the loaded database as a global
struct-like variable, matching its DBC/ARXML definition. You set its
signal fields, then call output() to actually transmit it:
variables
{
message SensorNodeStatus statusMsg; // declared from the database
byte aliveCounter;
}
on start
{
statusMsg.CoolantTemp = 850; // raw units - scaling is defined in the DBC
statusMsg.SupplyVoltage = 13800; // e.g. millivolts, per the signal's DBC scale
statusMsg.SensorValid = 1;
statusMsg.AliveCounter = aliveCounter;
output(statusMsg);
write("Sent SensorNodeStatus, alive=%d", aliveCounter);
}
output() queues the message for transmission on the bus (or the
simulated bus, if running offline) immediately. Whether the values you
write are "raw" or "physical" depends on how the message was declared —
CANoe supports both raw byte-level messages and signal-based access
where scaling is applied automatically; check which style a given
project uses before assuming units.
Sending a message periodically¶
Automotive networks are full of cyclic messages — status frames sent
every fixed interval regardless of anything else happening. A minimal
periodic transmitter using setTimer() and re-arming itself:
variables
{
message SensorNodeStatus statusMsg;
msTimer cycleTimer;
byte aliveCounter;
}
on start
{
aliveCounter = 0;
setTimer(cycleTimer, 100); // fire once, 100 ms from now
}
on timer cycleTimer
{
statusMsg.CoolantTemp = 850;
statusMsg.SensorValid = 1;
statusMsg.AliveCounter = aliveCounter;
output(statusMsg);
aliveCounter = (aliveCounter + 1) % 16; // 4-bit alive counter wraps at 16
setTimer(cycleTimer, 100); // re-arm for the next cycle
}
This pattern — fire, do the work, re-arm — is the standard way to build a cyclic sender in CAPL; there is no built-in "repeat every N ms" primitive, so re-arming inside the handler itself is the documented idiom.
Reacting to a received message¶
A command/response pattern, receiving a command frame and responding with an acknowledgment on a different message ID:
variables
{
message MotorCommand cmdMsg; // received
message MotorStatus statusReply; // sent in response
}
on message MotorCommand
{
write("Received MotorCommand: TargetSpeed=%d", this.TargetSpeed);
if (this.TargetSpeed > 4000)
{
write("Rejecting out-of-range TargetSpeed request");
statusReply.CommandAccepted = 0;
}
else
{
statusReply.CommandAccepted = 1;
}
output(statusReply);
}
this is only valid inside the on message block it belongs to — it
gives you read access to every signal/byte of the specific frame that
triggered the handler.
A signal-loss timeout pattern¶
A very common real test/simulation pattern: detect that a periodic
message has stopped arriving, and react. This combines on message (to
reset a timeout) with on timer (to detect its expiry):
variables
{
msTimer signalTimeout;
}
on start
{
setTimer(signalTimeout, 300); // arm: 300 ms of silence = fault
}
on message SensorNodeStatus
{
cancelTimer(signalTimeout); // frame arrived - message is alive
setTimer(signalTimeout, 300); // re-arm the watchdog
}
on timer signalTimeout
{
write("SensorNodeStatus timed out - no frame for 300 ms");
// In a real test module this would set a FAIL verdict (Level 2, Module 2)
}
This exact shape — cancel-and-rearm on every valid receipt, react on expiry — is how CAPL simulations and test modules implement the kind of signal-timeout failsafe logic described in Module 1's power-window example.
Cheat sheet¶
| Element | Purpose |
|---|---|
variables { } |
Declares globals for the whole CAPL program |
on start |
Runs once when the measurement begins |
on message <Name> |
Runs every time that message is received; this refers to it |
on timer <name> |
Runs once when a named msTimer expires |
output(msg) |
Transmits a message immediately |
setTimer(t, ms) |
Arms/re-arms a timer for ms milliseconds from now |
cancelTimer(t) |
Stops a pending timer before it fires |
write("...", args) |
Prints to the CANoe Write window — the basic debugging tool |
message <Type> var |
Declares a variable typed as a specific database message |
| Alive counter pattern | Increment and wrap (% N) each cycle so receivers detect a frozen sender |
| Cyclic sender pattern | on timer handler re-arms itself with setTimer() at the end |
| Timeout/watchdog pattern | on message cancels+rearms; on timer fires only on real silence |
How It Actually Works: CAPL's cooperative, single-threaded runtime¶
Every on start, on message, and on timer block you write looks
like it runs "whenever the event happens," as if they were independent
threads — but CANoe's CAPL runtime is single-threaded and
run-to-completion: only one event procedure executes at a time, and
once it starts, it runs to its closing brace before the runtime will
service the next queued event, no matter what fires in between. This is
why the two most common CAPL bugs — a write() call that never seems to
appear promptly, and a periodic sender that starts drifting under load —
both trace back to the same mechanism.
Internally, CANoe maintains a single event queue: every received
message, every timer expiry, every on key press gets pushed onto it in
the order it actually occurred (using the hardware timestamps from
Module 4), and the runtime pops and executes them strictly one at a
time. If your on message SensorNodeStatus handler happens to take,
say, 2 ms to run (a write() call, a loop, a nested function call), any
on timer cycleTimer event that was due to fire during that 2 ms simply
waits in the queue — it does not fire late by a scheduled amount, it
fires as soon as the runtime becomes free, so accumulated handler time
across many events directly becomes cumulative drift in your "every
100 ms" cyclic sender. This is precisely why the re-arm pattern in this
module's cyclic-sender example (setTimer(cycleTimer, 100) called
inside the handler, rather than a hypothetical periodic-repeat
primitive) is honest about what CAPL actually guarantees: each interval
is "100 ms after this handler happened to run," not "100 ms after the
previous nominal deadline" — so a chain of slow handlers produces real,
measurable period drift, not just isolated jitter.
This also answers the exercise's closing question directly: two on
timer handlers both calling output() on the same message is not a
race condition in the multithreaded sense — CAPL's run-to-completion
model makes that structurally impossible — but it is a realistic
logic bug, because whichever handler's event happened to be queued first
"wins" deterministically for that instant, silently overwriting whatever
signal values the other handler had set, with no warning and no
compiler diagnostic pointing at the conflict.
Exercise¶
Design (in CAPL, following the patterns above — you do not need to run it) a script for a simulated wheel speed sensor node that:
- Sends a
WheelSpeedStatusmessage every 20 ms containing a speed value and a 4-bit alive counter that increments and wraps correctly. - Listens for a
DiagnosticResetRequestmessage and, on receiving it, resets its alive counter to 0 and writes a message to the Write window confirming the reset. - Implements a 100 ms timeout watchdog on an incoming
BrakeAppliedmessage from another node — if it stops arriving, write a fault message and set a flag variablebrakeSignalLostto 1; clear the flag the moment the message resumes.
Write out the full variables block and all three on ... handlers.
Then, in one paragraph, explain what would happen to your alive counter
logic if two separate on timer handlers in the same program both
happened to call output() on the exact same message — is that a
realistic mistake, and why or why not?