C# for Engineering and Automotive Tools

C# is widely used for engineering software on Windows and Linux PCs: diagnostic testers, flashing tools, end-of-line (EOL) test stations, HIL test automation, CAN/LIN/Ethernet analysis tools, calibration front-ends and manufacturing tools.

Concept

C# is widely used for engineering software on Windows and Linux PCs: diagnostic testers, flashing tools, end-of-line (EOL) test stations, HIL test automation, CAN/LIN/Ethernet analysis tools, calibration front-ends and manufacturing tools.

How C# connects CAN interfaces Vendor SDKs (Vector XL, PEAK PCAN-Basic, Kvaser CANlib) are native DLLs → P/Invoke wrappers (Module 42) or vendor.NET assemblies Ethernet devices

TcpClient/UdpClient/Socket (Module 36): DoIP, SOME/IP, XCP on Ethernet, SCPI instruments

System.IO.Ports.SerialPort; USB-serial adapters; HID/USB via LibUsbDotNet or vendor APIs Test equipment

SCPI text commands over TCP (port 5025) or VISA; LXI instruments

UDS (ISO 14229) over ISO-TP (ISO 15765-2) on CAN or DoIP (ISO 13400); ODX/PDX descriptions Signal descriptions

DBC files (CAN), ARXML (AUTOSAR), A2L (calibration) Design rules that recur in every project:

Put hardware behind interfaces so tools can run against simulators in CI (every example below does this).

Use async I/O with timeouts everywhere; hardware *will* stop answering.

Log every request and response with timestamps — the log is the evidence when a test fails at night.

Parse binary data with BinaryPrimitives/spans, respecting byte order (CAN signals: Intel = little-endian, Motorola = big-endian).

Export results in standard formats (CSV, JUnit XML, JSON) for CI and reporting.

10 03 (extended session) 50 03... (P2/P2* timing) 22 F1 90 (VIN) 62 F1 90 <data> 19 02 <mask>

59 02 <availability> <DTC + status>... 71 01 <id> <result>

7F <sid> <NRC> — e.g. 0x31 requestOutOfRange, 0x33 securityAccessDenied, 0x78 responsePending (keep waiting up to P2*)

flowchart TB
    APP["C# test / diagnostic application"] --> API["Communication API (ICanBus, ITransport, IDuplexDevice)"]
    API --> CAN["CAN / CAN FD via vendor SDK (P/Invoke)"]
    API --> ETH["Automotive Ethernet: DoIP over TCP 13400, SOME/IP over UDP"]
    API --> SER["Serial / USB (System.IO.Ports)"]
    API --> INS["Test equipment: SCPI over TCP/VISA, power supplies, DMMs"]
    CAN --> ECU["Embedded ECU"]
    ETH --> ECU
    SER --> ECU

Why It Matters

This is where C# meets the EngiLipi domains: the same language features you learned (async, spans, records, pattern matching, channels, reflection, LINQ) become a CAN analyzer, a UDS tester and a test framework.

Syntax

public interface ITransport { ValueTask SendAsync(byte[] payload); Task<byte[]?> ReceiveAsync(TimeSpan timeout, CancellationToken ct = default); }
byte[]? r = await transport.ReceiveAsync(p2Timeout);
string result = r switch { null => "timeout", [0x7F, var sid, var nrc] => $"NRC 0x{nrc:X2}", _ => "positive" };

Example

61.1 Project 1 — CAN log analyzer Import an ASC-style log, parse frames, filter IDs, decode signals from a DBC-like database, compute per-ID statistics (count, cycle time, jitter), search for events, detect timeouts and export CSV.

61.2 Project 2 — UDS diagnostic tool Session control, ECU identification (read DIDs), DTC display and clearing, a routine with *response pending*, negative response decoding, request/response logging and timeout handling. The ECU is a simulator connected through an in-memory transport.

61.3 Project 3 — Serial communication tool Port discovery and configuration (baud rate, data bits, parity, stop bits, timeouts) with System.IO.Ports, ASCII and hex modes, checksum verification, and a hex dump. The terminal logic works against an IDuplexDevice so it can be tested with a loopback device.

61.4 Project 4 — ECU test automation framework Test cases (attributes + reflection, Module 37) → runner (sequencing, retry, timeout) → diagnostic API → simulated ECU, with assertions, console logging and JUnit XML export.

using System.Globalization;
using System.Text;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

// ---- 1. Import: generate a small Vector-ASC-style log (in real use: File.ReadLines("trace.asc")) ----
string logPath = Path.Combine(Path.GetTempPath(), "eh_trace.asc");
File.WriteAllLines(logPath, SampleLog());
Console.WriteLine($"imported {File.ReadLines(logPath).Count()} lines from {Path.GetFileName(logPath)}");

// ---- 2. Parse (lazily, line by line: works for multi-GB logs) ----
var parser = new AscParser();
List<CanFrame> frames = parser.Parse(File.ReadLines(logPath)).ToList();
Console.WriteLine($"parsed {frames.Count} frames, skipped {parser.SkippedLines} non-frame/invalid lines");

// ---- 3. Signal database (what a DBC file describes) ----
var db = new SignalDatabase
{
    new SignalDef(0x0CF00400, "EngineSpeed", StartBit: 24, Length: 16, Factor: 0.125, Offset: 0, Unit: "rpm"),
    new SignalDef(0x18FEEE00, "CoolantTemp", StartBit: 0, Length: 8, Factor: 1, Offset: -40, Unit: "degC"),
    new SignalDef(0x18FEF100, "VehicleSpeed", StartBit: 8, Length: 16, Factor: 1.0 / 256, Offset: 0, Unit: "km/h"),
};

// ---- 4. Filter + decode ----
Console.WriteLine();
Console.Wr
…
using System.Diagnostics;
using System.Threading.Channels;

// The tool talks to an ECU through an ITransport. Here the "ECU" is a simulator on an in-memory transport;
// on a bench the same client runs over ISO-TP on CAN (vendor SDK) or DoIP (TCP port 13400).
var (clientSide, ecuSide) = InMemoryTransport.CreatePair();
using var cts = new CancellationTokenSource();
var ecu = new EcuSimulator(ecuSide);
Task ecuTask = ecu.RunAsync(cts.Token);

var log = new List<string>();
var client = new UdsClient(clientSide, TimeSpan.FromMilliseconds(300), log);

Console.WriteLine($"session control (extended): {await client.DiagnosticSessionControlAsync(0x03)}");
Console.WriteLine($"ECU identification:");
foreach (var (did, name) in new[] { (0xF190, "VIN"), (0xF189, "SW version"), (0xF18C, "serial number") })
    Console.WriteLine($"  {name,-14} (DID 0x{did:X4}): {await client.ReadDataByIdentifierAsync((ushort)did)}");
Console.WriteLine($"unsupported DID 0x1234: {await client.ReadDataByIdentifierAsync(0x1234)}");

Console.WriteLine("DTCs (ReadDTCInformation 0x19 0x02, status mask 0x09):");
foreach (var dtc in await client.ReadDtcsAsync(0x09)) Console.WriteLine($"  {dtc}");
Console.WriteLine($"clea
…
#:package [email protected]
using System.IO.Ports;
using System.Text;

// Available ports on this machine (a USB-serial adapter appears as COMx on Windows, /dev/ttyUSBx on Linux)
string[] ports = SerialPort.GetPortNames();
Console.WriteLine($"serial ports found: {(ports.Length == 0 ? "none on this machine" : string.Join(", ", ports))}");

// Port configuration (opening requires a real device, so the tool works against an abstraction)
using var port = new SerialPort("COM3", baudRate: 115200, Parity.None, dataBits: 8, StopBits.One)
{
    Handshake = Handshake.None, ReadTimeout = 500, WriteTimeout = 500, NewLine = "\r\n",
};
Console.WriteLine($"configured {port.PortName}: {port.BaudRate} baud, {port.DataBits}{port.Parity.ToString()[0]}{(int)port.StopBits}, timeouts {port.ReadTimeout} ms, open: {port.IsOpen}");

// The tool logic depends on a Stream: SerialPort.BaseStream on hardware, a loopback stream in tests
var device = new LoopbackDevice();
var terminal = new SerialTerminal(device, Console.Out);
await terminal.SendAsciiAsync("AT+VER?");
await terminal.SendHexAsync("55 AA 01 03 10 20 30 63");   // last byte = 8-bit sum of the others
await terminal.SendHexAsync("55 AA 01 03 10
…
#:property PublishAot=false
using System.Diagnostics;
using System.Reflection;
using System.Xml.Linq;

// Test cases -> test runner -> diagnostic/CAN API -> hardware interface -> ECU
IEcuApi ecu = new SimulatedEcuApi();
var runner = new TestRunner(ecu, retries: 1, defaultTimeout: TimeSpan.FromMilliseconds(300));
TestReport report = await runner.RunAsync(typeof(VcuTests));

Console.WriteLine();
Console.WriteLine($"summary: {report.Passed} passed, {report.Failed} failed, {report.Results.Count} total in {report.Results.Sum(r => r.Attempts)} attempts");

// Result export in JUnit XML (understood by Jenkins, GitLab, Azure DevOps, GitHub Actions)
var junit = new XElement("testsuite",
    new XAttribute("name", "VcuTests"), new XAttribute("tests", report.Results.Count), new XAttribute("failures", report.Failed),
    report.Results.Select(r => new XElement("testcase", new XAttribute("name", r.Name),
        r.Passed ? null : new XElement("failure", new XAttribute("message", r.Message)))));
Console.WriteLine(junit);

// ================================================================= the tests (sequenced by [Order])
[TestSuite("VCU functional tests")]
public class VcuTests(IEcuApi ecu)
{
 
…

Expected Result

The two missing VehicleSpeed frames show up twice: as a 200 ms jitter in the statistics and as an explicit timeout finding. The 7E0 diagnostic request has no signal definition (?), and the ErrorFrame line was skipped by the parser.

The routine first answered 7F 31 78 (response pending); the client switched to the extended P2* timeout and received the final 71 01 FF 00 00 about 500 ms later. The log is exactly what a diagnostic engineer reads when investigating.

The port list shows the serial ports of the machine that ran the program; the tool itself only needs a port name. The corrupted checksum was detected before sending.

The flaky bus-load test passed on its retry, while the timeout and the real DTC failure were reported with clear messages — in the console and as JUnit XML that any CI system can display.

imported 17 lines from eh_trace.asc
parsed 14 frames, skipped 3 non-frame/invalid lines

decoded EngineSpeed (filter: id 0x0CF00400):
  t=  0.010s  EngineSpeed = 800 rpm
  t=  0.110s  EngineSpeed = 950 rpm
  t=  0.210s  EngineSpeed = 1100 rpm
  t=  0.310s  EngineSpeed = 1250 rpm
  t=  0.410s  EngineSpeed = 1400 rpm
  t=  0.510s  EngineSpeed = 1550 rpm

ID         name          count  cycle ms  jitter ms  dir
000007E0   ?                 1         -          -   Tx
0CF00400   EngineSpeed       6     100.0        0.0   Rx
18FEEE00   CoolantTemp       3     200.0        0.0   Rx
18FEF100   VehicleSpeed      4     166.7      200.0   Rx

search: first CoolantTemp > 100 degC at t=0.450s (110 degC)
timeout: VehicleSpeed missing between t=0.220s and t=0.520s (300 ms, expected 100 ms)
exported 13 decoded values to eh_trace.csv; first rows:
  time_s,signal,value,unit
  0.010,EngineSpeed,800,rpm
  0.020,VehicleSpeed,20,km/h
  0.050,CoolantTemp,90,degC
session control (extended): session 0x03 active, P2 50 ms
ECU identification:
  VIN            (DID 0xF190): EHBAUREV250000001
  SW version     (DID 0xF189): VCU_SW_2.4.1
  serial number  (DID 0xF18C): SN-000451
unsupported DID 0x1234: negative response to 0x22: 0x31 requestOutOfRange
DTCs (ReadDTCInformation 0x19 0x02, status mask 0x09):
  P0A80-00  status 0x09 (testFailed, confirmed)
  U0100-00  status 0x08 (confirmed)
clear DTCs (0x14): cleared -> remaining 0
routine with 'response pending' (0x78): routine 0xFF00 finished, result 0x00
ECU not responding: no response (timeout)

request/response log:
  Tx 1003
  Rx 5003003201F4
  Tx 22F190
  Rx 62F1904548424155524556323530303030303031
  Tx 22F189
  Rx 62F1895643555F53575F322E342E31
  Tx 22F18C
  Rx 62F18C534E2D303030343531
  Tx 221234
  Rx 7F2231
  Tx 190209
  Rx 5902FF0A800009C1000008
  Tx 14FFFFFF
  Rx 54
  Tx 190209
  Rx 5902FF
  Tx 3101FF00
  Rx 7F3178
  Rx 7101FF0000
  Tx 1001
     timeout after 300 ms
serial ports found: COM3, COM6, COM4, COM5
configured COM3: 115200 baud, 8N1, timeouts 500 ms, open: False
TX ascii: AT+VER?
RX ascii: FW 1.4.2
TX hex:   55AA010310203063  (checksum byte ok)
RX hex:   0655AA010310203063
TX hex:   55AA0103102030C9  (checksum byte WRONG, expected 63)
RX hex:   0655AA0103102030C9
TX ascii: STATUS?
RX ascii: OK 12.6V 25C
received bytes, hex dump:
  0000  46 57 20 31 2E 34 2E 32  FW 1.4.2
  0008  0D 0A 06 55 AA 01 03 10  ...U....
  0010  20 30 63 06 55 AA 01 03   0c.U...
  0018  10 20 30 C9 4F 4B 20 31  . 0.OK 1
  0020  32 2E 36 56 20 32 35 43  2.6V 25C
  0028  0D 0A                    ..
=== VCU functional tests ===
[PASS] 1. EnterExtendedSession
[PASS] 2. ReadVin
[PASS] 3. SupplyVoltageInRange
  retrying FlakyBusLoadRecoversOnRetry: bus load above 70%
[PASS] 4. FlakyBusLoadRecoversOnRetry (2 attempts)
  retrying SlowRoutineMustFinishWithinTimeout: timeout after 100 ms
[FAIL] 5. SlowRoutineMustFinishWithinTimeout - timeout after 100 ms (2 attempts)
  retrying NoActiveDtcs: expected '59 02 FF', got '59 02 FF 0A 80 00 09'
[FAIL] 6. NoActiveDtcs - expected '59 02 FF', got '59 02 FF 0A 80 00 09' (2 attempts)

summary: 4 passed, 2 failed, 6 total in 9 attempts
<testsuite name="VcuTests" tests="6" failures="2">
  <testcase name="EnterExtendedSession" />
  <testcase name="ReadVin" />
  <testcase name="SupplyVoltageInRange" />
  <testcase name="FlakyBusLoadRecoversOnRetry" />
  <testcase name="SlowRoutineMustFinishWithinTimeout">
    <failure message="timeout after 100 ms" />
  </testcase>
  <testcase name="NoActiveDtcs">
    <failure message="expected '59 02 FF', got '59 02 FF 0A 80 00 09'" />
  </testcase>
</testsuite>

Common Mistakes

Blocking reads without timeouts on hardware I/O.

Business logic calling vendor DLL functions directly (untestable without hardware).

Ignoring byte order and signal bit layout when decoding.

Treating 0x78 responsePending as a failure (or waiting forever).

Loading whole multi-GB logs into memory instead of streaming.

Retrying tests that failed for real reasons and hiding defects (retries must be reported, as above).

Best Practices

Transport interfaces + simulators; run the whole tool in CI without hardware.

Structured logs of every frame/request with timestamps; export results in standard formats.

Signal definitions as data (DBC/ARXML import), not hard-coded bit math.

Async pipelines with channels for high-rate acquisition (Module 35); spans for parsing (Module 41).

Version and trace test results to requirements (ASPICE, ISO 26262 evidence).

Interview Relevance

High for automotive/embedded tooling roles: "how would you design a CAN logger in C#?", "explain UDS request/response and NRC 0x78", "how do you call a vendor C API from C#?", "how do you test tool code without hardware?", "Intel vs Motorola byte order?".

Practice Questions

Extend the analyzer with Motorola (big-endian) signals and a DBC-file parser for BO_/SG_ lines.

Add SecurityAccess (0x27) seed/key to the UDS tool with a simple XOR key algorithm in the simulator.

Add a TesterPresent background task that sends 3E 80 every 2 seconds while a session is active.

Add a "retry only on timeout" option to the test runner and a CSV report.

Quick Quiz

Q1. (Multiple choice) What does the negative response 7F 22 31 mean? A) Service not supported B) Request out of range for ReadDataByIdentifier C) Security access denied D) Response pending

Answer: B (verified: unsupported DID 0x1234).

Q2. (Predict the output) Bytes 0A 80 in a ReadDTCInformation response (first two bytes of a DTC). Which DTC code?

Answer: P0A80 — bits 7–6 of the first byte select P/C/B/U (verified decoder output P0A80-00).

Q3. (True/False) After 7F 31 78 the tester should report a failure.

Answer: False. It means *response pending*: keep waiting (up to P2*) for the final response (verified).

Q4. (Find the bug) var lines = File.ReadAllLines("trace_8GB.asc");

Answer: Loads the entire log into memory; stream with File.ReadLines and a lazy parser (as the analyzer does).

Coding Challenge

Write a function that decodes an unsigned signal from a CAN payload given start bit, length, factor and offset for Intel (little-endian) byte order, and decode engine speed (start bit 24, length 16, factor 0.125) from FF 7D 7D 40 1F 00 00 00.

byte[] payload = [0xFF, 0x7D, 0x7D, 0x40, 0x1F, 0x00, 0x00, 0x00];
Console.WriteLine($"engine speed: {Decode(payload, startBit: 24, length: 16, factor: 0.125, offset: 0)} rpm");
Console.WriteLine($"byte 1 as temperature: {Decode(payload, startBit: 8, length: 8, factor: 1, offset: -40)} degC");

static double Decode(ReadOnlySpan<byte> data, int startBit, int length, double factor, double offset)
{
    ulong raw = 0;
    for (int i = 0; i < data.Length; i++) raw |= (ulong)data[i] << (8 * i);    // Intel: byte 0 is least significant
    ulong value = (raw >> startBit) & ((1UL << length) - 1);
    return value * factor + offset;
}