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 --> ECUWhy 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,degCsession 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 msserial 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;
}