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 thevspyx.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)