Getting Started with Diagnostics

This tutorial will cover the basic setup for a UDS (ISO-14229) diagnostics interface via DoIP (ISO-13400).

For this tutorial, it will be assumed that the network interface is already configured elsewhere and associated with a Python variable arbitrarily named network and a VSpy X application object named app.

Setup (Transport Layer)

Instatiating and configuring a DoIP client interface is fairly straightforward:

import vspyx

transportLayer = vspyx.Diagnostics.ISO13400_2.New()
transportLayer.Initialize(app, "DoIP")
transportLayer.Attach(network)

Note

The argument passed into Attach() must be a configured vspyx.TCPIP.Network object (see Extras for more info).

Add a handler for reception of DoIP NACK messages (optional):

def _OnDoIPNack(source, nackCode):
    print("Received nack code: ", nackCode)

transportLayer.OnDoIPNack = _OnDoIPNack

If the IP address or logical address of a target ECU are unknown, the information can be collected by the application when DoIP vehicle annoucements are made. Add a handler for reception of DoIP vehicle annoucements (optional):

def _OnVehicleAnnouncement(ipAddress, entity):
    AddEntityInfoToAddressbook(ipAddress, entity)

    print("ECU detected at IP address %s with logical address %s, VIN= %s"%(ipAddress, hex(entity.Address), [hex(octet) for octet in entity.VIN]))

transportLayer.OnVehicleAnnouncement = _OnVehicleAnnouncement

Note

In the above example, AddEntityInfoToAddressbook() is a suggested function that may perform an action such as recording the vehicle entity info in an application database, etc.

By default, the DoIP discovery broadcast address is 255.255.255.255 - if a different address is desired, it can be changed:

BROADCAST_ADDRESS = "192.168.7.255"

transportLayer.SetBroadcastAddress(BROADCAST_ADDRESS)

At this point, the transport layer is configured and can be handed off to the next layer!

Setup (Session Layer)

The session management layer manages session timeouts and keep-alive, message retransmission, and session changes. ISO-14229 standard session management is handled by an object of type vspyx.Diagnostics.ISO14229_2.

# Session layer configurations are being changed from default
sessionParameters = vspyx.Diagnostics.ISO14229_2.Parameters()
# sessionParameters.DeltaP2 = datetime.timedelta(milliseconds= 200)
# sessionParameters.DeltaP6 = datetime.timedelta(milliseconds= 200)
# sessionParameters.P2_server_max = datetime.timedelta(milliseconds= 250)
# sessionParameters.P2star_server_max = datetime.timedelta(milliseconds= 5000)
# sessionParameters.P2_client_max = datetime.timedelta(milliseconds= 250)
# sessionParameters.P6_client_max = datetime.timedelta(milliseconds= 250)
# sessionParameters.P2star_client_max = datetime.timedelta(milliseconds= 5000)
# sessionParameters.P6star_client_max = datetime.timedelta(milliseconds= 5000)

# Instantiate the session layer management object
session = vspyx.Diagnostics.ISO14229_2.NewClient(sessionParameters)
session.Initialize(app, "Session")
session.Attach(transportLayer)

Setup (Application Layer)

The application layer manages basic application message transactions, inter-message timeouts, and reception/relaying of any received responses or requests. ISO-14229 standard application client is handled by an object of type vspyx.Diagnostics.ISO14229_1ClientApplicationLayerProtocol.

udsInterface = vspyx.Diagnostics.ISO14229_1ClientApplicationLayerProtocol.New()
udsInterface.Initialize(app, "UDS Client")
udsInterface.Attach(session)

Setup (Application Services Layer)

The application services layer manages the highest-level service capabilities of a client or server. Its functionality includes management of available service configuration as well as standard service request validation and simplified service invocation. ISO-14229 standard application services client is handled by an object of type vspyx.Diagnostics.ISO14229_ServiceClient.

services = vspyx.Diagnostics.ISO14229_ServiceClient.New()
services.Initialize(app, "Service Client")
services.Attach(udsInterface)

Configuring the application services object to utilize services is done through the vspyx.Diagnostics.ISO14229_Services.ServiceConfig.ConfigureService() function.

Below is an example of configuring predefined, standard services for use.

# Configure predefined services
# ServiceConfig.ConfigureService(name, supported_sessions, p4server_max, supported_subfunctions= None, security_requirements= None)
service = services.ServiceConfig.ConfigureService("SessionControl", [ 1, 2, 3 ], datetime.timedelta(milliseconds= 10), [ 1, 2, 3 ], None)
service = services.ServiceConfig.ConfigureService("TesterPresent", [ 1, 2, 3 ], datetime.timedelta(milliseconds= 10), [ 0 ], None)
service = services.ServiceConfig.ConfigureService("ReadDataById", [ 1, 2, 3 ], datetime.timedelta(milliseconds= 30000), None, None)

Note

The argument p4server_max is used to indicate the max amount of time a service may execute. This argument is most important for a server object, as this value determines whether it is possible for the server to transmit NRC(RCRPR) (NRC 0x78) if the service takes longer than (P2_server_keepalive_ratio * P2_server_max).

Adding a custom service is similar to configuring a predefined one; however, the parameters must be completely provided and message interpreters must be provided.

# Add custom services

# Custom message interpreter handler
def _CreateMessageWithSubfunction(pdu, message):
    return vspyx.Diagnostics.ISO14229_Services.MessageWithSubfunction(pdu, message)

# ServiceConfig.AddService(sid, name, supported_sessions, datetime.timedelta(milliseconds= p4server_max), supported_subfunctions, security_requirements)
service = services.ServiceConfig.AddService(0xBA, "Echo", [ 1 ], datetime.timedelta(milliseconds= 5000), [ 1 ], 0xFFFFFFFF)
service.RequestDecoder = _CreateMessageWithSubfunction
service.ResponseDecoder = _CreateMessageWithSubfunction

Note

The RequestDecoder and ResponseDecoder handlers must return a type inheriting vspyx.Diagnostics.ISO14229_Services.MessageWithSubfunction

Starting Runtime

The components must be registered with the runtime environment:

runtime = app.VehicleSpy.PrepareForStart()
runtime.AddComponent(services)
runtime.AddComponent(udsInterface)
runtime.AddComponent(session)
runtime.AddComponent(network)
runtime.AddComponent(transportLayer)

Use During Runtime

UDS Services can be be invoked directly through the vspyx.Diagnostics.ISO14229_ServiceClient object. Addressing can be done either on a per-call basis, or by assigning the target address (N_AI) to the vspyx.Diagnostics.ISO14229_ServiceClient.TargetAddress attribute.

# N_AI => (MessageType, SA, TA, TA_Type, AE)
udsInterface.TargetAddress = (vspyx.Communication.ISOStandardizedServicePrimitiveInterface.MessageType.Diagnostics, SA, TA_PHYS, 0, None)

Invoking a service using the TargetAddress attribute is done by providing None as the target argument to a service call:

# Physical TesterPresent transaction, expecting response, implicit address (TargetAddress)
result = udsInterface.TesterPresent(None, False)

Below is an example of invoking a service using an explicit address:

# Functional N_AI
target_func = (vspyx.Communication.ISOStandardizedServicePrimitiveInterface.MessageType.Diagnostics, SA, TA_FUNC, 0, None)

...

# Functional TesterPresent transaction, expecting no responses, explicit address
result = udsInterface.TesterPresent(target_func, True)

Use case: SessionControl(Extended)

# Example: SessionControl
result = services.SessionControl(2, None)

if (result is None or len(result.Responses) == 0):
  print("No response received!")
else:
  message = result.Responses[0]
  if (message.IsNegativeResponse):
    responseCode = int(message.NRC)
    sid = int(message.FailedSID)
    print("Received NRC", hex(responseCode), "for service", hex(sid))
  else:
    print("SessionControl Response - Subfunction=", hex(message.Subfunction), "P2ServerMax=", message.P2ServerMax, "P2StarServerMax=", message.P2StarServerMax)

Use case: ReadDataById(…)

# Example: Read Data by Id
result = services.ReadDataById([ 0xF101, 0xF107 ], None)

if (result is None or len(result.Responses) == 0):
  print("No response received!")
else:
  message = result.Responses[0]
  if (message.IsNegativeResponse):
    responseCode = int(message.NRC)
    sid = int(message.FailedSID)
    print("Received NRC", hex(responseCode), "for service", hex(sid))
  else:
    handle = message.DataStart

    while (handle.IsValid()):
        did = message.ReadId(handle)
        signalSize = GetDIDSize(did)

        if (signalSize is None):
          # Unknown DID, unable to determine size
          signalSize = (handle.Size - handle.CurrentOffset)
          print("Unknown DID: %s"%(hex(did)))

        signalValue = message.ReadParameterData(handle, signalSize).bytes()

        print("DID", hex(did), "contains:", [hex(octet) for octet in signalValue])

Note

In the above example, function GetDIDSize() is assumed to be a user-defined script helper which resolves DID definition.

Use case: ECUReset(HardReset)

# Example: ECU Reset (hard reset)
result = services.EcuReset(1, None)

if (result is None or len(result.Responses) == 0):
  print("No response received!")
else:
  message = result.Responses[0]
  if (message.IsNegativeResponse):
    responseCode = int(message.NRC)
    sid = int(message.FailedSID)
    print("Received NRC", hex(responseCode), "for service", hex(sid))
  else:
    print("EcuReset Response - ResetType=", hex(message.ResetType))
    powerDownTime = message.PowerDownTime
    if (powerDownTime > 0):
        print("Invalid response! Unexpected PowerDownTime included ", powerDownTime)

Use case: Custom Service

# Example: Custom service
result = services.GenericService(vspyx.Core.BytesView(bytes([0xBA, 0x01, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10])), None)

if (result is None or len(result.Responses) == 0):
  print("No response received!")
else:
  message = result.Responses[0]
  if (message.IsNegativeResponse):
    responseCode = int(message.NRC)
    sid = int(message.FailedSID)
    print("Received NRC", hex(responseCode), "for service", hex(sid))
  else:
    print("Custom Echo Response - Subfunction=", hex(message.Subfunction), "payload=", [hex(octet) for octet in message.ToRaw().bytes()[2:]])

Shutting Down

The components will automatically shut down on application shutdown:

runtime.ShutdownEnvironment()
app.VehicleSpy.Stop()

Extras: Network Interface Configuration

Below is an example of configuring a network object utilizing a Windows Ethernet interface named Local Area Connection with MAC address d0:50:99:5b:fa:07 via pcap and assigning it its own MAC and IP addresses:

import vspyx
from intrepidcs.vspyx.rpc.TCPIP import Network_pb2

# Ethernet device name and MAC address
IF_AUTO_DETECT = True                     # Automatically detect network interface by name; otherwise, the device's MAC address must be provided
IF_NAME = "Local Area Connection"         # OS name of Ethernet interface device

IF_MAC_ADDRESS = "d0:50:99:5b:fa:07"      # Actual MAC address of the Ethernet interface device (only used if IF_AUTO_DETECT == False)
IF_SOFT_MAC_ADDRESS = "00:FC:70:00:00:01" # Client network virtual interface MAC address
IF_IP_ADDRESS = "192.168.7.100"           # Client network interface IP address
IF_IP_MASK = "255.255.255.0"              # Client network interface IP netmask

# Create application object if one already hasn't been created or provided
app = vspyx.Core.Application.New()
app.Initialize(loadAllModules=True)

if (IF_AUTO_DETECT):
    # This will resolve an existing pcap device and channel object
    interfaceDetectionAttempts = 10
    source = None
    while (interfaceDetectionAttempts > 0):
        try:
            interfaceDetectionAttempts -= 1
            source = app.Resolver[IF_NAME]
        except:
            print("Waiting for interface %s..."%(IF_NAME))
            time.sleep(1.0)
    if (source is None):
        print("Unable to resolve device %s"%(IF_NAME))
        exit()

    source = app.VehicleSpy.AddSource(source.Description)
    for child in source.Discovery.Children:
        if (isinstance(child, vspyx.Communication.EthernetChannel)):
            channel = child
            break
else:
    # This will initialize the pcap relationship with the specified device and resolve the channel object
    source = app.VehicleSpy.AddSource("pcap " + IF_MAC_ADDRESS)
    source.SearchFor()
    print(IF_NAME + " :", source.SourceState)
    if (source.SourceState != source.SourceState.Ready):
        print("Source is not ready!")
        exit()
    # Get the channel reference
    channel = app.Resolver[IF_NAME + " Discovery Channel"]

# Interface Configuration
interfaceConfig = Network_pb2.Interface()
interfaceConfig.MACAddress = IF_SOFT_MAC_ADDRESS
interfaceConfig.IPv4.Address = IF_IP_ADDRESS
interfaceConfig.IPv4.Netmask = IF_IP_MASK

# Instantiate and associate the interface device
interface = vspyx.TCPIP.Interface.New(interfaceConfig)
interface.Initialize(app, "Interface")
interface.Attach(channel)

# Instantiate and associate the network object
network = vspyx.TCPIP.Network.New()
network.Initialize(app, "Network")
network.AddInterface(interface)

Extras: DoIP Discovery

If the target DoIP entity addressing info is already known, simply add it to the DoIP transport layer’s entity table:

transportLayer.AddDoIPEntity(TARGET_ECU_IP_ADDRESS, TARGET_ECU_LOGICAL_ADDRESS)

If it is desired to manually broadcast an entity discover request:

entities = transportLayer.GetVehicleIdentification(None)  # Argument of None will broadcast; otherwise, providing an IP address will query a specific entity
print("Results of manual vehicle identification request (%d entities responded):"%(len(entities)))
for entityInfo in entities:
  (ipAddress, entity) = entityInfo
  print("ECU detected at IP address %s with logical address %s, VIN= %s"%(ipAddress, hex(entity.Address), [hex(octet) for octet in entity.VIN]))

It is also possible to query by EID:

QUERY_EID = vspyx.Core.BytesView(bytes([ 0xFA, 0x0A, 0x43, 0x99, 0xF7, 0xD7]))
entities = transportLayer.GetVehicleIdentificationByEID(QUERY_EID, None)  # Argument of None will broadcast; otherwise, providing an IP address will query a specific entity

or by VIN:

QUERY_VIN = vspyx.Core.BytesView(bytes([ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]))
entities = transportLayer.GetVehicleIdentificationByVIN(QUERY_VIN, None)  # Argument of None will broadcast; otherwise, providing an IP address will query a specific entity

Extras: Configuring for Use with CAN and ISO-15765

Similarly to using DoIP, a CAN device must be initialized and an ISO-15765 instance must be instantiated for use as the transport layer.

Device configuration of a client expecting ECU with 11-bit arb id 0x123 and 29-bit tester id 0x18FF456:

# Device and TP Configuration
class ISO15765_AddressTypes(enum.Enum):
    Normal = 1
    NormalFixed = 2
    Extended = 3

IF_LIB = "icsneo"   # Use this when using the icsneo library

IF_DEVICE = "88432"     # ValueCAN 3 - 88432
#IF_DEVICE = "CY7347"    # neoVI FIRE 2 - CY7347

IF_NAME = "%s %s"%(IF_LIB, IF_DEVICE)
IF_CHANNEL = "HSCAN"

NETWORK_ADDRESS_TYPE = ISO15765_AddressTypes.Normal

TESTER_ADDRESS = 0x45           # Specify the client external tester address
TESTER_CAN_ADDRESS = 0x18ff456
TESTER_ADDRESS_TYPE = vspyx.Communication.ISO15765_2.NetworkAddressType.PhysicalClassicalCAN29Bit

ECU_ADDRESS = 0x23              # Specify the ECU server address
ECU_CAN_ADDRESS = 0x123
ECU_ADDRESS_TYPE = vspyx.Communication.ISO15765_2.NetworkAddressType.PhysicalClassicalCAN11Bit

TESTER_DATA_LENGTH = 8          # Specify the client external tester DLC (in octets)
TESTER_STMIN = 0
TESTER_BS = 0
ECU_AE = None                   # Specify the ECU server address (N_AE)
ECU_STMIN_MIN = 0

...

# Configure the Device
channel = AutoDetectDevice(IF_NAME, 10, vspyx.Communication.CANChannel, IF_CHANNEL)

if (channel is None):
    print("Unable to initialize device: %s"%(IF_NAME))
    exit()

interface = channel.NewISO11898_1Interface()

# Create an ISO-15765 TP layer handler
transportLayer = vspyx.Communication.ISO15765_2.New(TESTER_DATA_LENGTH, 0)
transportLayer.Initialize(app, "ISO 15765-2")
transportLayer.Attach(interface)

# Note: Extended + RemoteDiagnostics is not possible
mType = vspyx.Communication.ISOStandardizedServicePrimitiveInterface.MessageType.Diagnostics if (ECU_AE is None)  else vspyx.Communication.ISOStandardizedServicePrimitiveInterface.MessageType.RemoteDiagnostics

# Configure the TP address filtering/handling
if (NETWORK_ADDRESS_TYPE == ISO15765_AddressTypes.Normal):
    transportLayer.AddRxNormalAddress(mType, ECU_ADDRESS, ECU_CAN_ADDRESS, ECU_ADDRESS_TYPE, TESTER_ADDRESS, TESTER_CAN_ADDRESS, TESTER_ADDRESS_TYPE, ECU_AE, TESTER_STMIN, TESTER_BS)
    transportLayer.AddTxNormalAddress(mType, TESTER_ADDRESS, TESTER_CAN_ADDRESS, TESTER_ADDRESS_TYPE, ECU_ADDRESS, ECU_CAN_ADDRESS, ECU_ADDRESS_TYPE, ECU_AE, ECU_STMIN_MIN)
elif (NETWORK_ADDRESS_TYPE == ISO15765_AddressTypes.NormalFixed):
    transportLayer.AddRxFixedAddress(mType, ECU_ADDRESS, TESTER_ADDRESS, TESTER_ADDRESS_TYPE, ECU_AE, False, TESTER_STMIN, TESTER_BS)
    transportLayer.AddTxFixedAddress(mType, TESTER_ADDRESS, ECU_ADDRESS, ECU_ADDRESS_TYPE, ECU_AE, False, ECU_STMIN_MIN)
elif (NETWORK_ADDRESS_TYPE == ISO15765_AddressTypes.Extended):
    transportLayer.AddRxExtendedAddress(vspyx.Communication.ISOStandardizedServicePrimitiveInterface.MessageType.Diagnostics, ECU_ADDRESS, ECU_CAN_ADDRESS, ECU_ADDRESS_TYPE, TESTER_ADDRESS, TESTER_CAN_ADDRESS, TESTER_ADDRESS_TYPE, None, TESTER_STMIN, TESTER_BS)
    transportLayer.AddTxExtendedAddress(vspyx.Communication.ISOStandardizedServicePrimitiveInterface.MessageType.Diagnostics, TESTER_ADDRESS, TESTER_CAN_ADDRESS, TESTER_ADDRESS_TYPE, ECU_ADDRESS, ECU_CAN_ADDRESS, ECU_ADDRESS_TYPE, None, ECU_STMIN_MIN)

...

# Getting ready to run...
targetAddress = ECU_ADDRESS
taType = int(ECU_ADDRESS_TYPE)
n_ae = ECU_AE

...

target_ai = (mType, TESTER_ADDRESS, targetAddress, taType, n_ae)

Examples