Typhoon HIL XIL API Python Guide

How to start to use ASAM XIL API for creating tests in a Python environment

Importing the XIL API .NET Assemblies

If you want to use .NET components and services, you have to add a reference of the required assembly into the Python workspace. To do this, you can use Python for .NET command clr.AddReference().
import sys
import clr
import os
import typhoon.api.hil as hil

ASSEMBLY_NAME = 'ASAM.XIL.Implementation.Testbench'
SERVER_CLASS_NAME = 'TyphoonHIL.ASAM.XIL.Server'

# Get TyphoonHIL SW version and system drive label for ASSEMBLY_ROOT path
sw_ver = hil.get_sw_version()
system_drive = os.getenv("SystemDrive")

ASSEMBLY_ROOT = r'{}\ProgramData\ASAM\XIL\Typhoon HIL Control Center {}' \
                r'\XILVersion_2.1.0\C#'.format(system_drive, sw_ver)
sys.path.append(str(ASSEMBLY_ROOT))

# Add reference to the XIL API .NET assemblies
clr.AddReference(ASSEMBLY_NAME)
clr.AddReference(SERVER_CLASS_NAME)

Creating a Typhoon HIL XIL API Testbench

All XIL API interfaces can be created via factory methods. A factory approach is applied in order to maximize independence of test cases from the test system. The Testbench API separates the test hardware from the test software and allows standardized access to the hardware.

You can create a Testbench using the following code:
import TyphoonHIL.ASAM.XIL.Server as server

testbench = server.Testbench()
print('Testbench invoked') 

Creating and Configuring a MA (Model Access) Port

The Testbench API covers access to the Model Access hardware. Model Access provides access to simulation model read and write parameters, capture functionality, and generated signals.

To load and apply the configuration to an MAPort instance, the LoadConfiguration and Configure methods are used.

Then, check the Typhoon HIL API Python code fragment:
import os
from pathlib import Path
import typhoon.api.hil as hil
from typhoon.api.schematic_editor import model

FILE_DIR_PATH = Path(__file__).parent  

model_name = "test_model.tse"
model_path = os.path.join(FILE_DIR_PATH, "hil_model", model_name) 

compiled_model_path = model.get_compiled_model_file(model_path)
model.load(model_path)

model.compile()
hil.load_model(compiled_model_path, vhil_device=vhil_enable)

and the corresponding XIL API .NET code fragment:
# script directory
TEST_DIR_PATH = Path(__file__).parent

MODEL_FILE_NAME = "test_model.tse"
MODEL_FILE_PATH = os.path.join(TEST_DIR_PATH, "hil_model", MODEL_FILE_NAME)

# Example of XML file
xml_content = f"""<?xml version='1.0'?>
<root>
  <schematic_path>{MODEL_FILE_PATH}</schematic_path>
  <vhil_device>false</vhil_device>
  <debug>true</debug>
</root>"""

XML_FILE_NAME = "ModelInfo_HIL.xml"
MA_PORT_CONFIG = os.path.join(TEST_DIR_PATH, "hil_model", XML_FILE_NAME)


# Invoke Testbench
testbench = server.Testbench()

# Create MA Port
maPort = testbench.MAPortFactory.CreateMAPort('ECU-TEST MA-Port')
    
# Create configuration file – because we want to use a relative MODEL_FILE_PATH
with open(MA_PORT_CONFIG, 'w') as xml_f:
    xml_f.write(xml_content) 

# Load configuration
cfg = maPort.LoadConfiguration(str(MA_PORT_CONFIG))

# Apply to the MA Port
maPort.Configure(cfg, False)

In order to maximize independence of test cases from test systems it is not sufficient to just standardize interfaces of a test system. Rather there should be a generic way to obtain corresponding instances from any vendor providing an XIL implementation. Therefore, the factory approach is applied. That means instances are obtained from methods of already existing objects.

MA Port - Reading and Writing Variables

The sequence diagram in Figure 1 shows how to handle and access model variables. We assume that the XIL simulator is already initialized, and a simulation model is running. An MAPort instance is used to collect all available model variables and to check their presence in the simulation model. Before accessing a model variable, we can check if a variable's data type and if it is readable or writable through the MAPort instance. The variable can be accessed by the Read() and the Write() method of the MAPort object.

Figure 1. MA Port Sequence Diagram
Below you can find an example of how to read / write variables:
# The model is a SCADA Input connected to a Probe
setVal = 10.0

# Check if variables are readable - return True or False
canRead = maPort.IsReadable('SCADA Input1')
# Check if variables are writable - return True or False
canWrite = maPort.IsWritable('SCADA Input1')
# Get the variable data type
varDataType = maPort.GetDataType('SCADA Input1')

# Write a value to the SCADA input
maPort.Write('SCADA Input1', testbenchVar.ValueFactory.CreateFloatValue(setVal))
# Read a value from the probe
probeExp = maPort.Read('Probe1')

MA Port - Start / Stop Simulation and Simulation States

When MAport configuration is finished, you can start and stop simulation on the way below:
# Start simulation – simulation can be started from MAport states: 
# eDISCONNECTED or eSIMULATION_STOPPED
maPort.StartSimulation()
# The current MAport state after starting simulation is eSIMULATION_RUNNING

# Run some tests
#      .
#      .
#      .

# Stop simulation – simulation can be stopped from MAport states: 
# eSIMULATION_RUNNING
maPort.StopSimulation()
# The current MAport state after stopping simulation is eSIMULATION_STOPPED

# Disconnect from MAPort – simulation will be stopped, if not before 
# Can be called from MAport states: eSIMULATION_RUNNING or eSIMULATION_STOPPED
maPort.Disconnect()
# The current MAport state after disconnecting from MAport is eDISCONNECTED

# The MAport State can be checked in the following way
currentState = maPort.State
print(“The current MAport state is: {}”.format(currentState))

maPort.Dispose()
print("MAPort DISPOSED")
To check the MAport state, you can define a maPortState dictionary with all possible states:
maPortState = {
    # A connection to the port's hardware is established. Model simulation 
    # is not running (stopped).
    0x0000: "eSIMULATION_STOPPED",
    # A connection to the port's hardware is established. Model simulation 
    # is running.
    0x0001: "eSIMULATION_RUNNING",
    # A connection to the port's hardware is not established.
    0x0002: "eDISCONNECTED",
}

# You can check the state of simulation by:
currentState = maPort.State

assert maPortState[currentState] == "eSIMULATION_RUNNING", "The MA Port" \ 
                                        "state is not in eSIMULATION_RUNNING state."

The MAport state diagram is shown in Figure 2. Here you can see which methods can be called from which states.

Figure 2. MAport state diagram

If you want to check which variables are in the model, you can use the VariableNames property:

# Get all variable names from the list of variable names
varNamesList = maPort.VariableNames

Or list of all task names using TaskNames property:

# Get all task names from the list of task names
taskNames = maPort.TaskNames

Capture - Create, Start, Stop Capture, and Capture States

The process of acquiring data in a continuous data stream is called CAPTURING. It guarantees that all process data can be retrieved as they occur related to the real-time service in respect to the capture service task. After completion of this process or even while it is still in progress, the data acquired can be retrieved.

The classes responsible for controlling Capture execution, as well as obtaining and displaying measured data, are accessible in the Common.Capturing package. These classes are used by MAPort instances.

The Capture class is the main class of the Capturing package. It is used to define control capture execution.

When capturing a Probe signal, signal streaming must be set. Enabling the Signal Streaming property allows acquisition and logging of the signal. An instance of the Capture class represents a capture definition. A capture is created by the port for which a capture shall be defined (MAPort in our case).

# Create and initialize a Capture object
taskName = "Capturing_Probe"
capture = maPort.CreateCapture(taskName)
# Get the current Capture State (should be "eCONFIGURED")
currentCaptureState = capture.State

# Set variables for capture
probeOutputLst = ["Probe1", "Subsystem1.Probe1"]
capture.Variables = Array[str](probeOutputLst)

# Create a CaptureResultMDFWriter object
captureResultMDFWriter =
    testbenchVar.CapturingFactory.CreateCaptureResultMDFWriterByFileName(file_path)

# Capturing process – START capturing
capture.Start(captureResultMDFWriter)
# Get the current Capture State (should be "eRUNNING")
currentCaptureState = capture.State

# Capturing process – Stop must be called to explicitly stop the capture process
capture.Stop()

Capture objects have a state representing the status of data acquisition. It can be retrieved via the Capture's state property.

file_path – path to the MDF file.

The State diagram for an untriggered capturing mode is shown in Figure 3.

Figure 3. State diagram for an untriggered Capture mode

Signal Segments - Creating Stimulus Signals

To create the signals and symbols of the stimulus domain of XIL API, use the SignalFactory and SymbolFactory classes instead of constructors. The available signal segments are shown below.
# Create Const segment
constSegment = testbench.SignalFactory.CreateConstSegment()
constSegment.Duration = testbench.SymbolFactory.CreateConstSymbolByValue(3.4)
constSegment.Value = testbench.SymbolFactory.CreateConstSymbolByValue(5)
# Get the Segment Type (Should be “eCONST”)
segmentTyp = constSegment.Type

# Create Exponential segment
expSegment = testbenchVar.SignalFactory.CreateExpSegment()
expSegment.Duration = testbenchVar.SymbolFactory.CreateConstSymbolByValue(5)
expSegment.Start = testbenchVar.SymbolFactory.CreateConstSymbolByValue(1)
expSegment.Stop = testbenchVar.SymbolFactory.CreateConstSymbolByValue(4)
expSegment.Tau = testbenchVar.SymbolFactory.CreateConstSymbolByValue(0.63)
# Get the Segment Type (Should be “eEXP”)
segmentTyp = expSegment.Type

# Create Ramp segment
rampSegment = testbenchVar.SignalFactory.CreateRampSegment()
rampSegment.Duration = testbenchVar.SymbolFactory.CreateConstSymbolByValue(5)
rampSegment.Start = testbenchVar.SymbolFactory.CreateConstSymbolByValue(1)
rampSegment.Stop = testbenchVar.SymbolFactory.CreateConstSymbolByValue(6)
# Get the Segment Type (Should be “eRAMP”)
segmentTyp = rampSegment.Type

# Create RampSlope segment
rampSlopeSegment = testbenchVar.SignalFactory.CreateRampSlopeSegment()
rampSlopeSegment.Duration = testbenchVar.SymbolFactory.CreateConstSymbolByValue(5)
rampSlopeSegment.Offset = testbenchVar.SymbolFactory.CreateConstSymbolByValue(1)
rampSlopeSegment.Slope = testbenchVar.SymbolFactory.CreateConstSymbolByValue(5)
# Get the Segment Type (Should be “eRAMPSLOPE”)
segmentTyp = rampSlopeSegment.Type

# Create Sine segment
sineSegment = testbenchVar.SignalFactory.CreateSineSegment()
sineSegment.Amplitude = testbenchVar.SymbolFactory.CreateConstSymbolByValue(10)
sineSegment.Duration = testbenchVar.SymbolFactory.CreateConstSymbolByValue(5)
sineSegment.Offset = testbenchVar.SymbolFactory.CreateConstSymbolByValue(1)
sineSegment.Period = testbenchVar.SymbolFactory.CreateConstSymbolByValue(0.25)
sineSegment.Phase = testbenchVar.SymbolFactory.CreateConstSymbolByValue(3)
# Get the Segment Type (Should be “eSINE”)
segmentTyp = sineSegment.Type

# Create Noise segment
noiseSegment = testbenchVar.SignalFactory.CreateNoiseSegment()
noiseSegment.Duration = testbenchVar.SymbolFactory.CreateConstSymbolByValue(10)
noiseSegment.Mean = testbenchVar.SymbolFactory.CreateConstSymbolByValue(5)
noiseSegment.Sigma = testbenchVar.SymbolFactory.CreateConstSymbolByValue(1)
noiseSegment.Seed = testbenchVar.SymbolFactory.CreateConstSymbolByValue(4)
# Get the Segment Type (Should be “eNOISE”)
segmentTyp = noiseSegment.Type

# Create Saw segment
sawSegment = testbenchVar.SignalFactory.CreateSawSegment()
sawSegment.Duration = testbenchVar.SymbolFactory.CreateConstSymbolByValue(10)
sawSegment.Offset = testbenchVar.SymbolFactory.CreateConstSymbolByValue(5)
sawSegment.Period = testbenchVar.SymbolFactory.CreateConstSymbolByValue(1)
sawSegment.Amplitude = testbenchVar.SymbolFactory.CreateConstSymbolByValue(4)
sawSegment.Phase = testbenchVar.SymbolFactory.CreateConstSymbolByValue(0)
sawSegment.DutyCycle = testbenchVar.SymbolFactory.CreateConstSymbolByValue(1)
# Get the Segment Type (Should be “eNOISE”)
segmentTyp = sawSegment.Type

# Create Pulse segment
pulseSegment = testbenchVar.SignalFactory.CreatePulseSegment()
pulseSegment.Duration = testbenchVar.SymbolFactory.CreateConstSymbolByValue(10)
pulseSegment.Offset = testbenchVar.SymbolFactory.CreateConstSymbolByValue(5)
pulseSegment.Period = testbenchVar.SymbolFactory.CreateConstSymbolByValue(1)
pulseSegment.Amplitude = testbenchVar.SymbolFactory.CreateConstSymbolByValue(4)
pulseSegment.Phase = testbenchVar.SymbolFactory.CreateConstSymbolByValue(0)
pulseSegment.DutyCycle = testbenchVar.SymbolFactory.CreateConstSymbolByValue(1)
# Get the Segment Type (Should be “eNOISE”)
segmentTyp = pulseSegment.Type

If you want to create a SignalValue signal segment then you need to create it manually in Typhoon HIL, with 3rd party tools, or by loading a segment from a .STI file.

The signal description file is used to serialize objects of type SignalDescriptionSet, and furthermore to serialize objects of type SignalGenerator. The signal description file is an XML file with the file extension STI.

Generating Stimulus Signals

In order to use signals for stimulation, a signal generator is used. A signal generator relates signals to model variables and controls the signal generation process.

All classes from the Common.SignalGenerator namespace are constructed via methods of the SignalGeneratorFactory class in the XIL API.
channels_list = [(["SampleSignal1"]), (["SampleSignal2"]), (["SampleSignal3"])]

# Read signals from STI file
stiReader = testbench.SignalGeneratorFactory.CreateSignalGeneratorSTIReaderByFileName(STI_FILE_PATH)

# Create Signal Generator
signalGenerator = maPort.CreateSignalGenerator()
# Load signal from STI file
signalGenerator.Load(stiReader)
# Check the state of SigGen – should be eIN_CONFIGURATION
sigGenState = signalGenerator.State

# Assign a signal (SampleSignal1) to the model variable
modelVarName1 = "SCADA Input1"
assignmentsDict = Dictionary[str, str]()
assignmentsDict[channels_list[0]] = modelVarName1

# Sets the mapping of signal descriptions to model variables
signalGenerator.Assignments = assignmentsDict
# Load signal to the Typhoon HIL device - switch to the state eREADY
signalGenerator.LoadToTarget()

# Start Signal Generator - switch to the state eRUNNING
signalGenerator.Start()

# Make some measurements - Capture the signal 
#                     .
#                     .
#                     .

# Pause signal generator before signal end - switch to the state ePAUSED
signalGenerator.Pause()

# Make some changes – for example change the signal
#                     .
#                     .
#                     .

# Start Signal Generator - switch to the state eRUNNING
signalGenerator.Start()

# Stop the Signal Generator – switch to the state eSTOPPED
signalGenerator.Stop()

A SignalGenerator defines stimuli and manages their execution. For the definition of a stimulus, a SignalDescriptionSet is referenced by the SignalGenerator. The signals from the SignalDescriptionSet are assigned with model variables in the "Assignments" collection. To manage stimuli, functionality is provided for downloading a stimulus to the target system, for starting, stopping, and pausing it and for observing its current state.

An example of a STI file structure with one signal is shown below:
<?xml version="1.0" encoding="utf-8"?>
<SignalDescriptionFile xmlns="http://www.asam.net/XIL/Signal/2.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.asam.net/XIL/Signal/2.2.0 ../SignalDescriptionFormat.xsd">
	<!-- Signal generator file demonstrating the assignment of signal descriptions to target system variables -->
	<SignalDescriptionSet>
		<SignalDescription name="SampleSignal1" id="ID_SIGNAL_SampleSignal1">
			<SegmentSignalDescription>
				<ConstSegment>
					<Duration xsi:type="ConstSymbol">
						<Value>1.0</Value>
					</Duration>
					<Value xsi:type="ConstSymbol">
						<Value>1</Value>
					</Value>
				</ConstSegment>
				<RampSegment>
					<Duration xsi:type="ConstSymbol">
						<Value>3.4</Value>
					</Duration>
					<Start xsi:type="ConstSymbol">
						<Value>1</Value>
					</Start>
					<Stop xsi:type="ConstSymbol">
						<Value>10</Value>
					</Stop>
				</RampSegment>
				<RampSlopeSegment>
					<Duration xsi:type="ConstSymbol">
						<Value>8</Value>
					</Duration>
					<Slope xsi:type="ConstSymbol">
						<Value>1</Value>
					</Slope>
					<Offset xsi:type="ConstSymbol">
						<Value>7.5</Value>
					</Offset>
				</RampSlopeSegment>
				<ExpSegment>
					<Duration xsi:type="ConstSymbol">
						<Value>4</Value>
					</Duration>
					<Start xsi:type="ConstSymbol">
						<Value>1</Value>
					</Start>
					<Stop xsi:type="ConstSymbol">
						<Value>5</Value>
					</Stop>
					<Tau xsi:type="ConstSymbol">
						<Value>0.5</Value>
					</Tau>
				</ExpSegment>
				<SineSegment>
					<Duration xsi:type="ConstSymbol">
						<Value>4</Value>
					</Duration>
					<Amplitude xsi:type="ConstSymbol">
						<Value>3</Value>
					</Amplitude>
					<Period xsi:type="ConstSymbol">
						<Value>2</Value>
					</Period>
					<Phase xsi:type="ConstSymbol">
						<Value>1</Value>
					</Phase>
					<Offset xsi:type="ConstSymbol">
						<Value>-1</Value>
					</Offset>
				</SineSegment>
                            <NoiseSegment>
                                    <Duration xsi:type="ConstSymbol">
                                          <Value>1.0</Value>
                                    </Duration>
                                    <Mean xsi:type="ConstSymbol">
                                          <Value>2.0</Value>
                                    </Mean>
                                    <Seed xsi:type="ConstSymbol">
                                          <Value>3.5</Value>
                                    </Seed>
                                    <Sigma xsi:type="ConstSymbol">
                                          <Value>0.5</Value>
                                    </Sigma>
                            </NoiseSegment>
                            <SawSegment>
                                    <Amplitude xsi:type="ConstSymbol">
                                         <Value>2.0</Value>
                                    </Amplitude>
                                    <Duration xsi:type="ConstSymbol">
                                         <Value>5.0</Value>
                                    </Duration>
                                    <DutyCycle xsi:type="ConstSymbol">
                                         <Value>0.5</Value>
                                    </DutyCycle>
                                    <Offset xsi:type="ConstSymbol">
                                         <Value>-2.0</Value>
                                    </Offset>
                                    <Period xsi:type="ConstSymbol">
                                         <Value>1.0</Value>
                                    </Period>
                                    <Phase xsi:type="ConstSymbol">
                                         <Value>0.25</Value>
                                    </Phase>
                            </SawSegment>
                            <PulseSegment>
                                    <Amplitude xsi:type="ConstSymbol">
                                         <Value>1.0</Value>
                                    </Amplitude>
                                    <Duration xsi:type="ConstSymbol">
                                         <Value>5.0</Value>
                                    </Duration>
                                    <DutyCycle xsi:type="ConstSymbol">
                                         <Value>0.9</Value>
                                    </DutyCycle>
                                    <Offset xsi:type="ConstSymbol">
                                         <Value>-2.0</Value>
                                    </Offset>
                                    <Period xsi:type="ConstSymbol">
                                         <Value>1.0</Value>
                                    </Period>
                                    <Phase xsi:type="ConstSymbol">
                                         <Value>0.25</Value>
                                    </Phase>
                            </PulseSegment>
			</SegmentSignalDescription>
		</SignalDescription>
	</SignalDescriptionSet>
</SignalDescriptionFile>

The format of the STI file is defined via an XML schema definition file. All signals and segments are serialized in their corresponding XML tags. Due to performance issues the numerical values of the SignalValueSegment are serialized in a separate MATLAB file (.mat) and not in the XML file.

Note: When using Signal Generator, Dynamic Model Stimulation must be turned on in Model Settings.