🔍
v1.3.8

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.

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);
LinePurpose
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.
Tip: Change 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).

Library: This is a library named 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:

StepCodePurpose
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
Timeout values: Use 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.

Library: 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:

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.

Compilation chain: This recipe requires the full library chain to compile: FB_STANDARD_LIB_V3UDS_DID_WAKE_UP_V2OPEL_INSIGNIA_B_FACELIFT_PASSIVE_CAN_IGNSTANDARD_FUNCTIONS_V1this 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

TypePurposeExample
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.

Library: 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.

Finding byte indices: Use the XBB app’s CAN monitor or TSharkRex AI to send UDS requests and analyze responses. The AI can help identify which bytes and bits correspond to which signals by correlating CAN data changes with physical actions (turning on high beam, pressing brake, etc.).

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.

Note

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));
Note: 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:

ParameterSourceDescription
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;
Note: 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.
Tip

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:

PatternUsed InPurpose
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