How to Communicate With CANbus Devices

This section explains the steps necessary to send and receive CAN bus messages in Squish tests.

CAN is a message bus standard which allows microcontroller and other devices (collectively called ECUs) to communicate without a central host or bus manager. It originates from automotive industry, but has been adopted for many other applications since.

The detailed description of the Squish CAN API can be found in the CAN bus support API documentation. In case testing requires complex interactions or require detailed modeling of ECUs, it may be more beneficial to use third party software which specializes in CAN bus simulations and can be coupled with Squish using the FMI Interface support.

In a typical test setup an embedded device which hosts the AUT would be connected to a CAN bus. In order to allow interaction with the bus in the test script, the test driver must have a compatible CAN controller connected to the same bus.

{}

Diagram of a Squish CAN bus test setup

CAN bus device

All references to the CAN interface should take place in its own application context which identifies the particular system connected to the bus. An application context which supports CAN bus API can be created using the ApplicationContext startCAN(options) function. Then you can establish a connection to the CAN controller and start sending and receiving messages.

    var canContext = startCAN();
    var device = new CanBusDevice("socketcan", "can0");
    var frame = new CanBusFrame(0x100, "4121999a");
    device.writeFrame(frame);
    canContext = startCAN()
    device = CanBusDevice("socketcan", "can0")
    frame = CanBusFrame( 0x100, "4121999a")
    device.writeFrame(frame)
    my $canContext = startCAN();
    my $device = CanBusDevice->new("socketcan", "can0");
    my $frame = CanBusFrame->new(0x100, "4121999a");
    $device->writeFrame(frame);
    canContext = startCAN();
    device = CanBusDevice.new("socketcan", "can0");
    frame = CanBusFrame.new(0x100, "4121999a");
    device.writeFrame(frame);
    set canContext [startCAN]
    set device [CanBusDevice new socketcan can0]
    set frame [CanBusFrame new 0x100 00deadbeef00]
    CanBusDevice invoke $device writeFrame $frame

The supported CAN drivers and the devices using it can be enumerated using the List CanBusDevice.pluginNames() and List CanBusDevice.availableDevices(driver) static methods.

    var canContext = startCAN();
    var plugins = CanBusDevice.pluginNames();
    for ( var i in plugins ) {
        var plugin = plugins[i];
        test.startSection(plugin);
        try {
            var devices = CanBusDevice.availableDevices(plugin);
            for ( var j in devices ) {
                test.log("Device: " + devices[j].deviceName);
            }
        } catch ( e ) {
            test.log("Failed: " + e.message );
        }
        test.endSection();
    }
    canContext = startCAN()
    plugins = CanBusDevice.pluginNames()
    for plugin in plugins:
        test.startSection(plugin)
        try:
            devices = CanBusDevice.availableDevices(plugin)
            for device in devices:
                test.log("Device: %s" % device.deviceName)
        except Exception as e:
            test.log("Failed: %s" % str(e))
        test.endSection()
    my $canContext = startCAN();
    my @plugins = CanBusDevice->pluginNames();
    foreach ( @plugins ) {
        my $plugin = $_;
        test::startSection($plugin);
        eval {
            @devices = CanBusDevice->availableDevices($plugin);
            foreach ( @devices ) {
                test::log("Device: $_->deviceName" );
            }
        } or {
            test::log("Failed: $@" );
        }
        test::endSection();
    }
    canContext = Squish.startCAN()
    plugins = CanBusDevice.pluginNames()
    plugins.each { |plugin|
        Test.startSection(plugin)
        begin
            devices = CanBusDevice.availableDevices(plugin)
            devices.each { |device|
                Test.log("Device: " + device.deviceName)
            }
        rescue Exception => e
            Test.log("Failed: " + e.message )
        end
        Test.endSection()
    }
    startCAN
    set plugins [invoke CanBusDevice pluginNames]
    foreach plugin $plugins {
        test startSection $plugin
        if { [catch {
            set devices [invoke CanBusDevice availableDevices $plugin]
            foreach device $devices {
                test log [concat "Device: " [property get $device deviceName]]
            }
        } err] } {
            test log [concat "Error: " $err ]
        }
        test endSection
    }

Frame contents

The CAN standard does not define the contents of the frame payloads - this is left to the designers of CAN networks and ECUs. Because of that, without any additional information Squish can only interpret frame payloads as a hexadecimal string. Since using such frame representation is very cumbersome, Squish offers a way to describe the contents of chosen frame types. In order to make use of it you can create a descriptor file which can be passed to the ApplicationContext startCAN(options) function.

    <canschema version="1">
      <frames>
        <frame id="0x100" name="Thermometer">
          <fields>
            <field name="temperature" type="floating" size="32"/>
          </fields>
        </frame>
        <frame id="0x200" name="AirConditioning">
          <fields>
            <field name="targetTemp" type="integral" size="32"/>
            <field name="cooler" type="integral" size="1"/>
            <field name="heater" type="integral" size="1"/>
          </fields>
        </frame>
      </frames>
    </canschema>

Using the above descriptor file, the frame members can be accessed in the test script.

    var canContext = startCAN({schema: File.open(fileName,"r").read()});
    var device = new CanBusDevice("socketcan", "can0");
    var frame = new ThermometerFrame();
    frame.temperature = 10.1;
    test.log(device.hexPayload); // Logs "4121999a"
    device.writeFrame(frame);
    canContext = startCAN()
    device = CanBusDevice("socketcan", "can0")
    frame = ThermometerFrame()
    frame.temperature = 10.1
    test.log(device.hexPayload) # Logs "4121999a"
    device.writeFrame(frame)
    my $canContext = startCAN();
    my $device = CanBusDevice->new("socketcan", "can0");
    my $frame = ThermometerFrame->new();
    $frame->temperature = 10.1;
    test::log($frame->hexPayload); # Logs "4121999a"
    $device->writeFrame(frame);
    canContext = startCAN();
    device = CanBusDevice.new("socketcan", "can0");
    frame = ThermometerFrame.new();
    frame.temperature = 10.1;
    Test.log(frame.hexPayload); # Logs "4121999a"
    device.writeFrame(frame);
    set canContext [startCAN]
    set device [CanBusDevice new socketcan can0]
    set frame [ThermometerFrame new]
    ThermometerFrame set $frame temperature 10.1
    test log [ThermometerFrame get hexPayload $th] # Logs "4121999a"
    invoke [$device writeFrame $frame]

The detailed desription of the allowed field types can be found in CAN frame schema documentation.

Sending CAN frames

Frames can be sent to a CAN device using the CanBusDevice.writeFrame(frame) function. However, it is a common practice to send important CAN frames repeatedly in short intervals. In order to simulate that behavior of particular ECU, you can create a CanBusFrameRepeater class object. Such object will send copies of the specified frame repeatedly for as long as it is enabled.

    var canContext = startCAN({schema: File.open(fileName,"r").read()});
    var device = new CanBusDevice("socketcan", "can0");
    var frame = new ThermometerFrame();
    frame.temperature = 10.1;
    var repeater = new CanBusFrameRepeater(device, frame);
    repeater.interval = 200; // 200ms interval
    canContext = startCAN(open(fileName, "r").read())
    device = CanBusDevice("socketcan", "can0")
    frame = ThermometerFrame()
    frame.temperature = 10.1
    repeater = CanBusFrameRepeater(device, frame)
    repeater.interval = 200 # 200ms interval
    my $canContext = startCAN();
    my $device = CanBusDevice->new("socketcan", "can0");
    my $frame = ThermometerFrame->new();
    $frame->temperature = 10.1;
    repeater = CanBusFrameRepeater->new(device, frame)
    repeater.interval = 200 # 200ms interval
    canContext = startCAN();
    device = CanBusDevice.new("socketcan", "can0");
    frame = ThermometerFrame.new();
    frame.temperature = 10.1;
    repeater = CanBusFrameRepeater.new(device, frame);
    repeater.interval = 200; # 200ms interval
    set canContext [startCAN]
    set device [CanBusDevice new socketcan can0]
    set frame [ThermometerFrame new]
    ThermometerFrame set $frame temperature 10.1
    set repeater [CanBusFrameRepeater new $device $frame]
    CanBusFrameRepeater set $repeater interval 200 # 200ms interval

You can modify the original frame object at any time during the test. The change will be immediately reflected in the repeater output.

    [...]
    // The measured temperature changes
    frame.temperature = 12.1;
    // or repeater.frame.temperature = 12.1;
    [...]
    # The measured temperature changes
    frame.temperature = 12.1
    # or repeater.frame.temperature = 12.1
    [...]
    # The measured temperature changes
    $frame->temperature = 12.1;
    # or $repeater->frame->temperature = 12.1;
    [...]
    # The measured temperature changes
    frame.temperature = 12.1;
    # or repeater.frame.temperature = 12.1;
    [...]
    # The measured temperature changes
    ThermometerFrame set frame temperature 12.1
    // or ThermometerFrame set [CanBusFrameRepeater get $repeater frame] temperature 12.1

Receiving frames

Receiving frames is possible using CanBusFrame CanBusDevice.readFrame(timeout) function. However, using it requires the test writer to inspect all incoming frames in a timely manner. While it offers the most versatility, it is not the most convenient tool in a typical test. Instead you can use a CanBusFrameReceiver class object. That object drains the incoming frames from the CAN device and keeps a history of received frames of specified IDs.

    var canContext = startCAN({schema: File.open(fileName,"r").read()});
    var device = new CanBusDevice("socketcan", "can0");
    var receiver = new CanBusFrameReceiver(device);
    receiver.setHistorySize(AirConditioningFrame.frameId, 1);
    snooze(5);
    // Logs the last-set temperature
    test.log(receiver.lastFrame(AirConditioningFrame.frameId).targetTemp);
    canContext = startCAN()
    device = CanBusDevice("socketcan", "can0")
    receiver = CanBusFrameReceiver(device)
    receiver.setHistorySize(AirConditioningFrame.frameId, 1)
    snooze(5)
    # Logs the last-set temperature
    test.log(receiver.lastFrame(AirConditioningFrame.frameId).targetTemp)
    my $canContext = startCAN();
    my $device = CanBusDevice->new("socketcan", "can0");
    my $receiver = CanBusFrameReceiver->new($device);
    $receiver->setHistorySize(Squish::AirConditioningFrame->frameId, 1);
    snooze(5);
    // Logs the last-set temperature;
    test::log($receiver->lastFrame(Squish::AirConditioningFrame->frameId)->targetTemp);
    canContext = startCAN();
    device = CanBusDevice.new("socketcan", "can0");
    receiver = CanBusFrameReceiver.new(device);
    receiver.setHistorySize(AirConditioningFrame.frameId, 1);
    snooze(5);
    # Logs the last-set temperature
    Test.log(receiver.lastFrame(AirConditioningFrame.frameId).targetTemp)
    set canContext [startCAN]
    set device [CanBusDevice new socketcan can0]
    set receiver [CanBusFrameReceiver new $device]
    CanBusFrameReceiver invoke $receiver setHistorySize [AirConditioningFrame get frameId] 1
    snooze 5
    # Logs the last-set temperature
    set lastFrame [CanBusFrameReceiver invoke $receiver lastFrame [AirConditioningFrame get frameId]]
    test log [AirConditioningFrame get $lastFrame targetTemp]

It is also possible to wait for a frame of specific ID and with specific field values.

    [...]
    receiver.setHistorySize(AirConditioningFrame.frameId, 1);
    var frame = receiver.waitForFrame({frameId: AirConditioningFrame.frameId, targetTemp: 18});
    test.log("Expected frame received");
    [...]
    receiver.setHistorySize(AirConditioningFrame.frameId, 1)
    var frame = receiver.waitForFrame({"frameId": AirConditioningFrame.frameId, "targetTemp": 18})
    test.log("Expected frame received")
    [...]
    $receiver->setHistorySize(Squish::AirConditioningFrame->frameId, 1);
    my %query = (frameId => Squish::AirConditioningFrame->frameId, targetTemp => 18);
    var frame = $receiver->waitForFrame(%query);
    test::log("Expected frame received");
    [...]
    receiver.setHistorySize(AirConditioningFrame.frameId, 1);
    frame = receiver.waitForFrame(("frameId"=>Squish::AirConditioningFrame->frameId,
                                   "targetTemp"=>18));
    Test.log("Expected frame received");
    [...]
    set frameId [AirConditioningFrame get frameId]
    CanBusFrameReceiver invoke $receiver setHistorySize $frameId 1
    set frame [CanBusFrameReceiver invoke waitForFrame (frameId $frameId targetTemp 18)]
    test log "Expected frame received"

The CanBusFrameReceiver.waitForFrame(filter, timeout) function searches the current history to find a matching frame, and if none is found it waits until a matching frame is received. This prevents a situation where the waiting API is called too late and misses a just-received frame.

© 2022 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.