Code Examples
This chapter contains five real production recipes from the XBB database. They progress from the simplest possible program to a full BMW DID module. Each example is annotated with explanations of patterns, naming conventions, and compilation requirements.
Example 1: Blinking Output
The absolute simplest recipe - a timer that blinks an output every 500 ms. No CAN bus, no vehicle, just a blinking light. This is perfect for verifying that the dongle and PowerUnit hardware work correctly before attempting a real vehicle recipe.
This recipe compiles and runs standalone - no libraries needed.
// ============================================================
// BLINKING OUTPUT - Simplest possible recipe
// Toggles an output at 1 Hz (500 ms on, 500 ms off)
// ============================================================
VAR
TMR_BLINK : TON; // Timer - counts up to PT, then sets Q = TRUE
BLINK_STATE : BOOL; // Tracks whether the output is currently on or off
END_VAR;
VAR_OUTPUT
BLINK_OUTPUT : OUTPUT; // Physical output on the PowerUnit
END_VAR;
// --- Self-resetting timer pattern ---
// TMR_BLINK starts counting when IN = TRUE.
// When it reaches PT (500 ms), Q becomes TRUE.
// We feed "NOT TMR_BLINK.Q" back into IN, so the timer
// restarts itself every cycle after Q fires.
TMR_BLINK(IN := NOT TMR_BLINK.Q, PT := T#500ms);
// When the timer fires, toggle the blink state and reset
IF TMR_BLINK.Q THEN
BLINK_STATE := NOT BLINK_STATE; // Toggle: TRUE → FALSE → TRUE ...
TMR_BLINK(IN := FALSE); // Reset the timer so it starts counting again
END_IF;
// --- Drive the output ---
// VALUE is 0..1000 (permille). BLINK_STATE is 0 or 1,
// so multiplying by 1000 gives full on (1000) or off (0).
// PERIOD = 1000 means the output runs at full duty cycle (no PWM dimming).
BLINK_OUTPUT(VALUE := BLINK_STATE * 1000, PERIOD := 1000);
How It Works
| Line | Purpose |
|---|---|
TMR_BLINK : TON |
Declares a rising-edge timer (Timer ON-delay). It starts counting when
IN goes TRUE and sets Q after PT
milliseconds. |
BLINK_STATE : BOOL |
A simple boolean flag to track the current on/off state. |
BLINK_OUTPUT : OUTPUT |
Declares a physical output. VALUE controls brightness
(0–1000 permille), PERIOD controls the PWM cycle length in ms. |
IN := NOT TMR_BLINK.Q |
Self-resetting pattern - the timer runs as long as it hasn’t fired yet. Once Q is TRUE, IN becomes FALSE, stopping the timer until we manually reset it. |
TMR_BLINK(IN := FALSE) |
Explicitly resets the timer’s internal elapsed time so it can start counting from zero again on the next cycle. |
T#500ms to adjust the blink speed. For example,
T#250ms gives 2 Hz (fast blink), T#1000ms gives 0.5 Hz
(slow blink). The total period is always 2 × PT.
Example 2: Passive CAN Reading (Opel Insignia)
This is the CAN reading library from Recipe 1576 (Opel Insignia B Facelift). It demonstrates the standard pattern for reading multiple CAN IDs from the vehicle’s broadcast CAN bus without sending any requests - hence “passive”.
This library reads three CAN IDs and extracts eight signals: high beam, DRL, brake light, ignition, and four gear positions (P/R/N/D).
OPEL_INSIGNIA_B_FACELIFT_PASSIVE_CAN_IGN. It compiles as part of a recipe
together with the standard library chain (FB_STANDARD_LIB, WAKE_UP, STANDARD_FUNCTIONS).
It cannot compile standalone.
// ============================================================
// OPEL INSIGNIA B FACELIFT - Passive CAN Library
// Recipe 1576
// Reads: highbeam, DRL, brake, ignition, gear P/R/N/D
// ============================================================
VAR
// --- CAN ID 0x140: Lights ---
CAN_DATA140 : ARRAY [0..7] OF BYTE; // 8-byte receive buffer
CAN_RECV140 : CAN_RX; // CAN receiver function block
CAN_TMR140 : TON; // Timeout timer
// --- CAN ID 0x1F5: Ignition ---
CAN_DATA1F5 : ARRAY [0..7] OF BYTE;
CAN_RECV1F5 : CAN_RX;
CAN_TMR1F5 : TON;
// --- CAN ID 0x197: Gear position ---
CAN_DATA197 : ARRAY [0..7] OF BYTE;
CAN_RECV197 : CAN_RX;
CAN_TMR197 : TON;
END_VAR
VAR_SIGNAL
// Signals exposed to the main recipe and the app
SIGNAL_HIGHBEAM : BOOL; // Helljus
SIGNAL_DRL : BOOL; // Varselljus / daytime running lights
SIGNAL_BRAKELIGHT : BOOL; // Bromsljus
SIGNAL_CAN_TANDNING : BOOL; // Tändning via CAN
SIGNAL_GEAR_P : BOOL; // Växel P (parkering)
SIGNAL_GEAR_R : BOOL; // Växel R (back)
SIGNAL_GEAR_N : BOOL; // Växel N (neutral)
SIGNAL_GEAR_D : BOOL; // Växel D (drive)
END_VAR
// === CAN ID 0x140 - Light signals ===
// Enable the receiver for standard (non-extended) CAN ID 0x140
CAN_RECV140(ENABLE := TRUE, ID := 0x140, EXT := FALSE, DATA := CAN_DATA140);
// WHILE loop drains ALL queued messages for this CAN ID.
// Multiple messages may arrive between execution cycles.
WHILE CAN_RECV140.AVAILABLE DO
// Bit access: CAN_DATA140[byte].bit
// Byte 0, bit 7 = high beam
SIGNAL_HIGHBEAM := CAN_DATA140[0].7;
// Byte 3, bit 7 = DRL
SIGNAL_DRL := CAN_DATA140[3].7;
// Byte 0, bit 6 = brake light
SIGNAL_BRAKELIGHT := CAN_DATA140[0].6;
// Reset timeout timer on every received message
CAN_TMR140(IN := FALSE);
// Call CAN_RECV again to advance to next queued message
CAN_RECV140();
END_WHILE
// Timeout: if no message received for 5 seconds, clear signals.
// This handles the case where the vehicle is turned off and
// CAN traffic stops - without timeout, signals would stay
// stuck in their last state forever.
CAN_TMR140(IN := TRUE, PT := T#5000ms);
IF CAN_TMR140.Q THEN
SIGNAL_HIGHBEAM := FALSE;
SIGNAL_DRL := FALSE;
SIGNAL_BRAKELIGHT := FALSE;
END_IF
// === CAN ID 0x1F5 - Ignition ===
CAN_RECV1F5(ENABLE := TRUE, ID := 0x1F5, EXT := FALSE, DATA := CAN_DATA1F5);
WHILE CAN_RECV1F5.AVAILABLE DO
// Byte 6, bit 3 = ignition on/off
SIGNAL_CAN_TANDNING := CAN_DATA1F5[6].3;
CAN_TMR1F5(IN := FALSE);
CAN_RECV1F5();
END_WHILE
CAN_TMR1F5(IN := TRUE, PT := T#1500ms);
IF CAN_TMR1F5.Q THEN
SIGNAL_CAN_TANDNING := FALSE;
END_IF
// === CAN ID 0x197 - Gear position ===
CAN_RECV197(ENABLE := TRUE, ID := 0x197, EXT := FALSE, DATA := CAN_DATA197);
WHILE CAN_RECV197.AVAILABLE DO
// Gear is encoded as a specific byte value in byte 5:
// 0x06 = Park, 0x0E = Reverse, 0x16 = Neutral, 0x1E = Drive
SIGNAL_GEAR_P := CAN_DATA197[5] = 0x06;
SIGNAL_GEAR_R := CAN_DATA197[5] = 0x0E;
SIGNAL_GEAR_N := CAN_DATA197[5] = 0x16;
SIGNAL_GEAR_D := CAN_DATA197[5] = 0x1E;
CAN_TMR197(IN := FALSE);
CAN_RECV197();
END_WHILE
CAN_TMR197(IN := TRUE, PT := T#1500ms);
IF CAN_TMR197.Q THEN
SIGNAL_GEAR_P := FALSE;
SIGNAL_GEAR_R := FALSE;
SIGNAL_GEAR_N := FALSE;
SIGNAL_GEAR_D := FALSE;
END_IF
The Passive CAN Pattern
Every passive CAN reading block follows the same three-part pattern. This is the most important pattern in TSharkRex - you will see it in every recipe that reads broadcast CAN data:
| Step | Code | Purpose |
|---|---|---|
| 1. Declare | CAN_RX + ARRAY[0..7] + TON |
Each CAN ID needs its own receiver, data buffer, and timeout timer. |
| 2. Read | WHILE CAN_RECV.AVAILABLE DO ... END_WHILE |
The WHILE loop drains the message queue. Multiple messages can arrive between execution cycles, so you must process all of them. |
| 3. Timeout | IF CAN_TMR.Q THEN ... END_IF |
If no message arrives for PT milliseconds, reset signals to safe defaults. Without this, signals stay stuck when the vehicle turns off. |
Signal Extraction: Bit Access
Signals are extracted using byte.bit notation:
// CAN_DATA[byte_index].bit_index
SIGNAL_HIGHBEAM := CAN_DATA140[0].7; // Byte 0, bit 7 (MSB)
SIGNAL_DRL := CAN_DATA140[3].7; // Byte 3, bit 7
// Full byte comparison for multi-value signals:
SIGNAL_GEAR_P := CAN_DATA197[5] = 0x06; // Byte 5 == 0x06 means Park
T#1500ms for ignition and gear signals
(need to respond quickly when vehicle shuts off). Use T#5000ms for light
signals (less critical, and some vehicles send light CAN at low frequency).
Example 3: Passive CAN Ignition (BMW G20)
This is one of the simplest possible CAN reading libraries - the ignition detection library from Recipe 1439 (BMW 3-Series G20). It reads a single CAN ID, extracts one bit as the ignition signal, and has a 1.5-second timeout.
This demonstrates the minimal version of the passive CAN pattern: one CAN ID, one signal, one timeout.
BMW_G20_UDS_PASSIVE_CAN_IGN. This compiles as part
of a recipe chain and cannot run standalone. It provides
SIGNAL_CAN_TANDNING to the main recipe.
// ============================================================
// BMW G20 - Passive CAN Ignition Library
// Recipe 1439
// Reads ignition status from CAN ID 0x130
// ============================================================
VAR
CAN_DATA : ARRAY [0..7] OF BYTE; // Receive buffer
CAN_RECV : CAN_RX; // CAN receiver
CAN_TMR : TON; // Timeout timer
END_VAR
VAR_SIGNAL
SIGNAL_CAN_TANDNING : BOOL; // Tändning (ignition on/off)
END_VAR
// Read CAN ID 0x130 with MSG_COUNT := 1
// MSG_COUNT limits the queue to 1 message - we only care about
// the latest ignition state, not historical values.
CAN_RECV(ENABLE := TRUE, ID := 0x130, EXT := FALSE, DATA := CAN_DATA, MSG_COUNT := 1);
WHILE CAN_RECV.AVAILABLE DO
// Byte 0, bit 2 = ignition status on the BMW G20
SIGNAL_CAN_TANDNING := CAN_DATA[0].2;
CAN_TMR(IN := FALSE); // Reset timeout on each received message
CAN_RECV(); // Advance to next message in queue
END_WHILE
// If no CAN message for 1.5 seconds, assume vehicle is off
CAN_TMR(IN := TRUE, PT := T#1500ms);
IF CAN_TMR.Q THEN
SIGNAL_CAN_TANDNING := FALSE;
END_IF
Comparison with Opel Example
Notice how this is the exact same pattern as Example 2, but stripped to the minimum:
- One CAN ID instead of three
- One signal instead of eight
- Same three-part structure: declare → WHILE read → timeout
MSG_COUNT := 1is used here because ignition is a state signal - only the latest value matters, not intermediate changes
Example 4: Main Recipe with Outputs (Opel Insignia)
This is a simplified version of the main recipe from Recipe 1576. The main recipe is the top layer in the compilation chain - it connects signals from libraries (CAN reading, standard functions) to physical outputs on the PowerUnit.
This example demonstrates signal routing, the high beam latch pattern, and output mapping.
FB_STANDARD_LIB_V3 → UDS_DID_WAKE_UP_V2 →
OPEL_INSIGNIA_B_FACELIFT_PASSIVE_CAN_IGN →
STANDARD_FUNCTIONS_V1 → this recipe.
Variables like SIGNAL_STANDARD_HIGHBEAM, SIGNAL_SYSTEM_ON, and
SIGNAL_VOLTAGE come from the standard libraries.
// ============================================================
// OPEL INSIGNIA B FACELIFT - Main Recipe (simplified)
// Recipe 1576
// Connects CAN signals to PowerUnit outputs
// ============================================================
// --- Ignition source selection ---
// Combine CAN-based ignition with hardware input ignition.
// Either source can wake the system - OR logic ensures
// the dongle activates if CAN OR input wire detects ignition.
SIGNAL_TANDNING := SIGNAL_CAN_TANDNING OR SIGNAL_IN_TANDNING;
// --- Reverse signal source ---
// Use gear position from CAN library (SIGNAL_GEAR_R from Example 2)
INPUT_REVERSE := SIGNAL_GEAR_R;
VAR
R_TRIG_HBEAM : R_TRIG; // Rising edge detector
F_TRIG_HBEAM : F_TRIG; // Falling edge detector
HBEAM_LATCH : BOOL; // Latched high beam state
END_VAR;
VAR_OUTPUT
HIGHBEAM : OUTPUT; // Helljus - PowerUnit output
IGNITION : OUTPUT; // Tändning - status output
REVERSELIGHT : OUTPUT; // Backljus
SYSTEM_ACTIVE : OUTPUT; // System aktiv - heartbeat
CAN_CONTROLLER : OUTPUT; // CAN-status
VOLTAGE : INFO_VALUE; // Spänning - display only, no physical output
LOW_BATTERY : INFO_VALUE; // Lågt batteri - warning indicator
ANALOG_IN0 : INFO_VALUE; // Analog ingång 0
END_VAR;
// === High Beam Latch Pattern ===
// The high beam lever on most vehicles is momentary - it sends a
// brief pulse when pulled. We need to detect the exact moment it
// activates (rising edge) and deactivates (falling edge) to get
// instant response without delay.
//
// R_TRIG.Q is TRUE for exactly ONE cycle when CLK goes FALSE → TRUE
// F_TRIG.Q is TRUE for exactly ONE cycle when CLK goes TRUE → FALSE
R_TRIG_HBEAM(CLK := SIGNAL_HIGHBEAM);
F_TRIG_HBEAM(CLK := SIGNAL_HIGHBEAM);
// Latch ON at the instant high beam activates
IF R_TRIG_HBEAM.Q THEN
HBEAM_LATCH := TRUE;
END_IF;
// Latch OFF at the instant high beam deactivates
IF F_TRIG_HBEAM.Q THEN
HBEAM_LATCH := FALSE;
END_IF;
// === Map signals to physical outputs ===
// High beam: VAL_CLAMP limits the value to 0..1000 range.
// The boolean-to-int conversion (HBEAM_LATCH > 0) * 1000 gives
// either 0 or 1000. SIGNAL_STANDARD_HIGHBEAM is added as a
// fallback from the standard library.
HIGHBEAM(VALUE := VAL_CLAMP(((HBEAM_LATCH > 0) * 1000) + SIGNAL_STANDARD_HIGHBEAM, 0, 1000), PERIOD := 1000);
// Ignition: simple boolean-to-permille conversion
IGNITION(VALUE := SIGNAL_STANDARD_IGNITION * 1000, PERIOD := 1000);
// Reverse light: SIGNAL_STANDARD_REVERSELIGHT is already 0..1000
REVERSELIGHT(VALUE := SIGNAL_STANDARD_REVERSELIGHT, PERIOD := 1000);
// === System status outputs ===
// These outputs report system health to the app
// System active: combination of system-on flag and uptime counter
SYSTEM_ACTIVE(VALUE := SIGNAL_SYSTEM_ON * SIGNAL_SYSTEM_TIME);
// CAN controller status (0 = off, 1 = on, 2 = error)
CAN_CONTROLLER(VALUE := SIGNAL_CAN_STATUS);
// Voltage in volts (raw value is in centimillivolts, divide by 100)
VOLTAGE(VALUE := SIGNAL_VOLTAGE / 100);
// Low battery warning flag
LOW_BATTERY(VALUE := SIGNAL_LOW_BATT);
// Analog input 0 (raw ADC value divided by 100)
ANALOG_IN0(VALUE := SIGNAL_ANALOG_IN0 / 100);
The High Beam Latch Pattern
The R_TRIG / F_TRIG latch is used when you need instant response to a signal change. Without it, there would be a delay equal to the CAN polling interval. With edge detection, the output changes on the exact cycle the signal transitions:
// Timeline:
// Cycle 1: SIGNAL_HIGHBEAM = FALSE, R_TRIG.Q = FALSE → no change
// Cycle 2: SIGNAL_HIGHBEAM = TRUE, R_TRIG.Q = TRUE → LATCH ON (instant!)
// Cycle 3: SIGNAL_HIGHBEAM = TRUE, R_TRIG.Q = FALSE → no change (already latched)
// Cycle 4: SIGNAL_HIGHBEAM = FALSE, F_TRIG.Q = TRUE → LATCH OFF (instant!)
OUTPUT vs INFO_VALUE
| Type | Purpose | Example |
|---|---|---|
OUTPUT |
Drives a physical output pin on the PowerUnit. VALUE is 0–1000 (permille), PERIOD controls PWM. | HIGHBEAM, IGNITION |
INFO_VALUE |
Display-only value shown in the app. No physical output - purely informational. | VOLTAGE, LOW_BATTERY |
Example 5: BMW DID Module (BMW G20)
This is the most complex example - the DID (Data Identifier) module from Recipe 1439 (BMW 3-Series G20). DID modules use UDS (Unified Diagnostic Services) to actively request specific data from vehicle ECUs, as opposed to passively listening for broadcast CAN messages.
This library sends a UDS ReadDataByIdentifier request to the BMW light module ECU and extracts four light signals from the multi-frame response.
BMW_G20_UDS_DID_LIGHT_MODULE_V1. This compiles as
part of a recipe chain and requires FB_STANDARD_LIB_V3 (which defines
DID_EXT) and the wake-up library (which provides
SIGNAL_TANDNING). It cannot compile standalone.
// ============================================================
// BMW G20 - DID Light Module Library
// Recipe 1439 (BMW_G20_UDS_DID_LIGHT_MODULE_V1)
// Reads: helljus, halvljus, backljus, bromsljus from ECU 0x640
// ============================================================
// === Constants - define the CAN addressing for this ECU ===
VAR_CONSTANT
MODULE_CAN_SEND_ID : UDINT := 0x6F1; // TX CAN ID (shared BMW diagnostic ID)
MODULE_CAN_RECV_ID : UDINT := 0x640; // RX CAN ID (light module response)
MODULE_CAN_EXT_ID : BOOL := FALSE; // Standard (not extended) CAN IDs
MODULE_ECU_ID : BYTE := MODULE_CAN_RECV_ID BAND 0xFF; // 0x40 - ECU address byte
MODULE_MSG_SHIFT : BYTE := 1; // BMW always shifts 1 byte (ECU ID prefix)
TOTALMSG : BYTE := 1; // Number of DID requests to this module
MODULE_0_DATA_LEN : INT := 19; // Expected response data length in bytes
END_VAR;
// === Buffers - storage for response data and request payloads ===
VAR
// Response buffer - DID_EXT writes decoded response data here
MODULE_0_DATA : ARRAY[0..MODULE_0_DATA_LEN - 1] OF BYTE;
// UDS request payload (one row per DID request):
// 0x40 = ECU ID, 0x03 = payload length, 0x22 = ReadDataByIdentifier,
// 0xD5, 0x42 = DID number (0xD542 = light status on BMW)
PAYLOAD : ARRAY[0..TOTALMSG - 1, 0..7] OF BYTE :=
[
0x40, 0x03, 0x22, 0xD5, 0x42, 0x00, 0x00, 0x00
];
// Round-robin order for multiple DIDs (only one here)
ORDER : ARRAY[0..0] OF BYTE := [0];
TOTALORDER: BYTE := 1;
END_VAR;
// === Output signals - exposed to the main recipe and app ===
VAR_SIGNAL
SIGNAL_HELLJUS : BYTE; // Helljus (high beam) status
SIGNAL_HALVLJUS : BYTE; // Halvljus (low beam / DRL) status
SIGNAL_BACKLJUS : BYTE; // Backljus (reverse light) status
SIGNAL_BROMSLJUS : BYTE; // Bromsljus (brake light) status
ENABLE_LJUS : BOOL; // Module enable flag
END_VAR;
// === Internal working variables ===
VAR
MODULES : ARRAY[0..TOTALMSG - 1] OF DID_EXT; // DID protocol handler(s)
CANRECV : CAN_RX; // CAN receiver for responses
RECVDATA : ARRAY[0..7] OF BYTE; // Raw CAN receive buffer
INIT : BOOL; // One-shot initialization flag
RES : INT; // Result/status from DID_EXT
PRIO : BYTE; // Current priority level
COUNTER : BYTE; // Round-robin counter
END_VAR
// === Initialization - runs once on first execution ===
// DID_EXT modules need extensive configuration. This block sets up
// pointers and parameters for each module, then initializes the
// CAN receiver. The INIT flag ensures this only runs once.
IF NOT INIT THEN
FOR i : BYTE := 0 TO TOTALMSG - 1 DO
// TX_ENABLE: only send requests when ignition is on
MODULES[i].TX_ENABLE := @SIGNAL_TANDNING;
MODULES[i].ACTIVE := MODULES[i].TX_ENABLE;
// Pointer to CAN receiver's availability flag
MODULES[i].NEW_DATA := @CANRECV.AVAILABLE;
// Pointer to raw received CAN data
MODULES[i].IN_DATA := RECVDATA;
// Pointer to the UDS request payload for this module
MODULES[i].PAYLOAD := @PAYLOAD[i, 0];
// BMW message shift (1 byte ECU ID prefix in responses)
MODULES[i].MSGSHIFT := MODULE_MSG_SHIFT;
// ECU ID for filtering responses
MODULES[i].ECUID := MODULE_ECU_ID;
// Priority value for round-robin scheduling
MODULES[i].PRIO_VAL := i;
MODULES[i].PRIO_P := @PRIO;
// CAN TX settings
MODULES[i].TX_ID := MODULE_CAN_SEND_ID;
MODULES[i].TX_EXT := MODULE_CAN_EXT_ID;
// Shared result variable
MODULES[i].RESULT := @RES;
END_FOR;
// Configure module 0 (light status DID):
MODULES[0](
ACTIVE := @ENABLE_LJUS, // Enable flag for this specific module
OUT_DATA := MODULE_0_DATA, // Where to write decoded response
LEN_OFFSET := 0, // No offset in response length
OUT_DATA_LEN := MODULE_0_DATA_LEN, // Expected 19 bytes of data
INIT_PAUSE := T#0ms, // No initial delay
PAUSE := T#0ms, // No pause between requests
EXTPAUSE := HW.HARDWARE_TYPE = XBB_DONGLE, // Extended pause for dongle hardware
EXTPAUSE_TIME := 0x01); // Extended pause duration
// Initialize the CAN receiver for responses from this ECU
CANRECV(ENABLE := TRUE, EXT := MODULE_CAN_EXT_ID, ID := MODULE_CAN_RECV_ID, DATA := RECVDATA);
INIT := TRUE;
END_IF;
// === Main processing loop ===
// DO-WHILE processes all pending CAN responses in a single cycle.
// This is critical for multi-frame UDS responses where several
// CAN frames arrive in rapid succession.
DO
RES := 0;
FOR i : INT := 0 TO TOTALMSG - 1 DO
// Call each DID_EXT module to process its state machine
MODULES[i]();
// If a module completed (RES > 0), advance round-robin
IF RES > 0 THEN
PRIO := ORDER[COUNTER];
COUNTER := COUNTER + 1;
IF COUNTER > TOTALORDER - 1 THEN COUNTER := 0; END_IF;
RES := 0;
END_IF;
END_FOR
// Check for more CAN messages
CANRECV();
WHILE CANRECV.AVAILABLE > 0 END_WHILE
// === Signal extraction ===
// After DID_EXT has decoded the multi-frame UDS response,
// the raw data is in MODULE_0_DATA[0..18].
// The byte indices correspond to specific signals in the
// BMW light module response for DID 0xD542:
SIGNAL_HELLJUS := MODULE_0_DATA[8].0; // Byte 8, bit 0 = high beam
SIGNAL_HALVLJUS := MODULE_0_DATA[6].0; // Byte 6, bit 0 = low beam
SIGNAL_BACKLJUS := MODULE_0_DATA[18].0; // Byte 18, bit 0 = reverse
SIGNAL_BROMSLJUS := MODULE_0_DATA[15].0; // Byte 15, bit 0 = brake
Key Concepts
CAN Addressing
BMW uses a shared diagnostic CAN ID 0x6F1 for sending requests to all ECUs.
Each ECU responds on its own CAN ID - the light module responds on 0x640.
The first byte of the response is always the ECU ID (0x40), which is why
MODULE_MSG_SHIFT = 1 - DID_EXT needs to skip that byte when parsing
the UDS protocol.
UDS Payload Breakdown
// PAYLOAD = [0x40, 0x03, 0x22, 0xD5, 0x42, 0x00, 0x00, 0x00]
// | | | | |
// | | | +--- DID 0xD542 (light status)
// | | +--- UDS service 0x22 (ReadDataByIdentifier)
// | +--- Payload length (3 bytes after this)
// +--- ECU ID (0x40 = light module)
The DO-WHILE Processing Loop
UDS multi-frame responses consist of multiple CAN frames that arrive in rapid succession.
The DO-WHILE loop ensures all frames are processed in a single execution
cycle. Without it, the DID_EXT state machine might miss continuation frames and fail
to reassemble the complete response.
Signal Extraction Indices
The byte indices in MODULE_0_DATA come from analyzing the actual UDS
response from the BMW light module. After DID_EXT strips the UDS protocol bytes
(service ID, DID echo, length), the remaining data bytes contain the signal values
at fixed positions. These positions are determined during vehicle reverse-engineering
by sending the UDS request and examining the raw response.
Example 6: Analog Input Ignition (Dongle-2)
Some vehicles (notably VAG - Volkswagen, Audi, Skoda, Seat, Cupra, Porsche)
have a +12V ignition signal available on pin 1 of the OBD-II connector.
XBB Dongle-2 can read this via its analog input. The PLC_INPUT function
converts the raw analog voltage into a clean TRUE/FALSE signal.
This only works on vehicles that actually provide a +12V ignition signal on OBD-II pin 1. This is common on VAG vehicles but not available on all cars. Check your vehicle’s OBD-II pinout before relying on this method.
// Analog ignition detection for Dongle-2
// Wire the vehicle's ignition (+12V when ON) to Dongle-2 pin 1
VAR_SIGNAL
HW : HARDWARE;
SIGNAL_IN_TANDNING : BOOL; // Clean ignition signal from analog input
END_VAR;
VAR_OUTPUT
IGNITION : OUTPUT;
VOLTAGE : INFO_VALUE;
ANALOG : INFO_VALUE;
END_VAR;
HW();
// PLC_INPUT compares analog input against supply voltage
// Returns TRUE when analog input has ~12V (ignition ON)
// Returns FALSE when analog input is ~0V (ignition OFF)
// Built-in hysteresis prevents flickering at threshold
SIGNAL_IN_TANDNING := PLC_INPUT(HW.SUPPLY_VOLTAGE, HW.ANALOG_IN0);
// Output the ignition signal
IGNITION(VALUE := SIGNAL_IN_TANDNING * 1000, PERIOD := 1000);
// Display voltages in app (divide mV by 100 for display)
VOLTAGE(VALUE := TRUNC(HW.SUPPLY_VOLTAGE / 100, INT));
ANALOG(VALUE := TRUNC(HW.ANALOG_IN0 / 100, INT));
PLC_INPUT is provided by the standard library
(NEW_FB_STANDARD_LIB_V3). This example requires the standard library
chain to compile.
How PLC_INPUT Works
PLC_INPUT takes two parameters:
| Parameter | Source | Description |
|---|---|---|
HW.SUPPLY_VOLTAGE |
Battery voltage (mV) | Reference - what “full voltage” looks like |
HW.ANALOG_IN0 |
Analog input pin (mV) | The signal to evaluate |
The function returns TRUE when the analog input voltage is close to the
supply voltage (meaning the ignition wire is live), and FALSE when it drops
to near zero. It includes built-in hysteresis so the signal doesn’t flicker when
the voltage is near the threshold.
Combining Analog + CAN Ignition (VAG Pattern)
In VAG recipes, you want the analog input on Dongle-2 but fall back to CAN on Dongle-1 (which has no analog input). The standard pattern checks the hardware type:
VAR_SIGNAL
INPUT_WAKE_UP : BOOL := FALSE; // Set by hardware check below
SIGNAL_IN_TANDNING : BOOL; // From PLC_INPUT (analog)
SIGNAL_CAN_TANDNING : BOOL; // From CAN/UDS reading
SIGNAL_TANDNING : BOOL; // Combined result
ENABLE_CAN_TANDNING : BOOL; // Enable CAN ignition reading
HW : HARDWARE;
END_VAR;
HW();
// Auto-detect hardware: enable analog input on Dongle-2, disable on Dongle-1
IF HW.HARDWARE_TYPE <> XBB_DONGLE THEN
INPUT_WAKE_UP := TRUE; // Dongle-2/PP-CAN: use analog input
END_IF;
// Only read ignition via CAN when analog is not available
ENABLE_CAN_TANDNING := NOT INPUT_WAKE_UP;
// Combine both ignition sources
// On Dongle-2: SIGNAL_IN_TANDNING comes from PLC_INPUT (analog pin)
// On Dongle-1: SIGNAL_IN_TANDNING is always FALSE, CAN is used instead
SIGNAL_TANDNING := SIGNAL_CAN_TANDNING OR SIGNAL_IN_TANDNING;
INPUT_WAKE_UP and SIGNAL_IN_TANDNING
are managed by the standard wake-up library. This code belongs in the main recipe layer
and requires the full library chain.
When INPUT_WAKE_UP := TRUE is set, the standard wake-up library
automatically calls PLC_INPUT and sets SIGNAL_IN_TANDNING.
The pattern HW.HARDWARE_TYPE <> XBB_DONGLE ensures Dongle-1 always
falls back to CAN-based ignition, while Dongle-2 and PP-CAN use the reliable analog input.
Pattern Summary
All six examples build on a small set of reusable patterns:
| Pattern | Used In | Purpose |
|---|---|---|
| Self-resetting timer | Example 1 | Create periodic events without external clock |
| Passive CAN read + timeout | Examples 2, 3 | Read broadcast CAN signals with safety timeout |
| Edge detection latch | Example 4 | Instant response to signal transitions |
| Signal-to-output mapping | Example 4 | Connect processed signals to physical outputs |
| DID_EXT UDS request | Example 5 | Actively query ECUs for diagnostic data |
| Analog input (PLC_INPUT) | Example 6 | Read ignition from wired analog signal on Dongle-2 |