J2534-Sharp.Core 2.0.4

dotnet add package J2534-Sharp.Core --version 2.0.4
                    
NuGet\Install-Package J2534-Sharp.Core -Version 2.0.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="J2534-Sharp.Core" Version="2.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="J2534-Sharp.Core" Version="2.0.4" />
                    
Directory.Packages.props
<PackageReference Include="J2534-Sharp.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add J2534-Sharp.Core --version 2.0.4
                    
#r "nuget: J2534-Sharp.Core, 2.0.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package J2534-Sharp.Core@2.0.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=J2534-Sharp.Core&version=2.0.4
                    
Install as a Cake Addin
#tool nuget:?package=J2534-Sharp.Core&version=2.0.4
                    
Install as a Cake Tool

J2534-Sharp

J2534-Sharp handles all the details of operating with unmanaged SAE J2534 spec library and lets you deal with the important stuff.

Available on NuGet! [NuGet Gallery: J2534-Sharp]

J2534-Sharp v2.0 - Modern .NET Rewrite

This is a complete rewrite of J2534-Sharp targeting .NET 10 with modern C# features, zero-allocation hot paths, and a Result pattern for error handling.

Key Improvements

🚀 Performance

  • Zero heap allocations in message send/receive hot path
  • Stack-allocated buffers using Span<T> and stackalloc
  • Pinned managed arrays for the message buffer (one allocation per channel, reused)
  • Single-copy data path: native buffer → Message.Data array

🎯 Modern .NET Features

  • .NET 10 with C# 14
  • Span<T> and ReadOnlySpan<T> for zero-copy data handling
  • unsafe code for direct native memory access
  • NativeLibrary API instead of kernel32 P/Invoke
  • Records for data carriers (APIInfo, CarDAQInfo)
  • Nullable reference types enabled

📦 Simplified Architecture

  • Eliminated 8 Heap* classes - replaced with stack locals and NativeMessageBuffer
  • Eliminated 3 Common/ base classes - simple IDisposable pattern
  • Message as readonly struct - immutable, stack-friendly
  • J2534Result<T> - Railway-oriented programming pattern for error handling

🔧 Breaking Changes (By Design)

  • Namespace remains SAE.J2534 - same as before
  • Class names changed: APIJ2534API, DeviceJ2534Device, ChannelJ2534Channel
  • Result pattern everywhere - no more exceptions for expected outcomes
  • ReadOnlySpan<byte> parameters instead of IEnumerable<byte>
  • Modern async-ready (though J2534 itself is synchronous)

Quick Start

Loading an API

using SAE.J2534;

// Discover registered APIs
foreach (var info in J2534APIFactory.DiscoverAPIs())
{
    Console.WriteLine($"{info.Name}: {info.FileName}");
}

// Load a specific API
var apiResult = J2534APIFactory.LoadAPI(@"C:\path\to\j2534.dll");
if (!apiResult.IsSuccess)
{
    Console.WriteLine($"Failed to load API: {apiResult.ErrorMessage}");
    return;
}

using var api = apiResult.Value;
Console.WriteLine($"Loaded {api.Signature}");

Opening a Device and Channel

// Open device
var deviceResult = api.OpenDevice();
if (!deviceResult.IsSuccess)
{
    Console.WriteLine($"Failed to open device: {deviceResult.ErrorMessage}");
    return;
}

using var device = deviceResult.Value;
Console.WriteLine($"Device: {device.DeviceName}");
Console.WriteLine($"Firmware: {device.FirmwareVersion}");

// Open channel
var channelResult = device.OpenChannel(
    Protocol.ISO15765,
    Baud.ISO15765_500000,
    ConnectFlag.NONE);

if (!channelResult.IsSuccess)
{
    Console.WriteLine($"Failed to open channel: {channelResult.ErrorMessage}");
    return;
}

using var channel = channelResult.Value;

Sending and Receiving Messages

// Send a message (zero allocation with stackalloc)
Span<byte> txData = stackalloc byte[] { 0x01, 0x00 };
var sendResult = channel.SendMessage(txData);

if (!sendResult.IsSuccess)
{
    Console.WriteLine($"Send failed: {sendResult.ErrorMessage}");
    return;
}

// Receive messages
var rxResult = channel.ReadMessages(10, timeoutMs: 1000);

if (rxResult.IsSuccess)
{
    foreach (var msg in rxResult.Messages)
    {
        Console.WriteLine($"RX: {BitConverter.ToString(msg.Data)}");
    }
}
else if (rxResult.IsTimeout)
{
    Console.WriteLine("No messages received (timeout)");
}

Using the Result Pattern

// Pattern matching
var result = channel.GetConfig(Parameter.DATA_RATE);
result.Match(
    onSuccess: value => Console.WriteLine($"Data rate: {value}"),
    onError: (code, msg) => Console.WriteLine($"Error {code}: {msg}")
);

// Chaining operations
var voltage = device.MeasureBatteryVoltage()
    .Map(v => v / 1000.0) // Convert mV to V
    .GetValueOrDefault(0.0);

Console.WriteLine($"Battery: {voltage:F2}V");

// Railway-oriented programming
var configResult = channel.GetConfig(Parameter.LOOPBACK)
    .Bind(value => channel.SetConfig(Parameter.LOOPBACK, value == 0 ? 1 : 0))
    .Bind(_ => channel.GetConfig(Parameter.LOOPBACK));

if (configResult.IsSuccess)
{
    Console.WriteLine($"Loopback toggled to: {configResult.Value}");
}

Message Filters

// Create a pass-all filter
var filter = new MessageFilter(UserFilterType.PASSALL, Array.Empty<byte>());
var filterResult = channel.StartMessageFilter(filter);

if (filterResult.IsSuccess)
{
    Console.WriteLine($"Filter started with ID: {filterResult.Value}");
}

// ISO15765 flow control filter
var iso15765Filter = new MessageFilter(
    UserFilterType.STANDARDISO15765,
    new byte[] { 0x00, 0x00, 0x07, 0xE8 } // Source address
);

var filterResult2 = channel.StartMessageFilter(iso15765Filter);

Periodic Messages

// Start a periodic message
var periodicMsg = new PeriodicMessage(
    data: new byte[] { 0x3E, 0x00 }, // Tester present
    interval: 2000 // Every 2 seconds
);

var periodicResult = channel.StartPeriodicMessage(periodicMsg);

if (periodicResult.IsSuccess)
{
    Console.WriteLine($"Periodic message started with ID: {periodicResult.Value}");
    
    // Let it run...
    Thread.Sleep(10000);
    
    // Stop it
    channel.StopPeriodicMessage(periodicResult.Value);
}

Architecture Overview

J2534APIFactory (static)
    ↓ LoadAPI()
J2534API (IDisposable)
    ↓ OpenDevice()
J2534Device (IDisposable)
    ↓ OpenChannel()
J2534Channel (IDisposable)
    → NativeMessageBuffer (pinned array, reused)

Memory Management

  • API: Holds NativeLibrary handle, disposed when API is disposed
  • Device: Tracks child channels, disposes them on device disposal
  • Channel: Owns a NativeMessageBuffer, freed on channel disposal
  • NativeMessageBuffer: Pinned byte[] via GCHandle, unpinned on dispose

Thread Safety

  • All API calls are locked via a shared _syncRoot object (per API instance)
  • Each channel can optionally have its own lock (future enhancement)
  • J2534APIFactory cache is thread-safe

Migration Guide from v1.x

v1.x v2.0
APIFactory.GetAPI() J2534APIFactory.LoadAPI()
API.GetDevice() J2534API.OpenDevice()
Device.GetChannel() J2534Device.OpenChannel()
Channel.GetMessages() Channel.ReadMessages()
Channel.SendMessage(IEnumerable<byte>) Channel.SendMessage(ReadOnlySpan<byte>)
try { api.Method(); } catch (J2534Exception) var result = api.Method(); if (!result.IsSuccess)
HeapMessageArray NativeMessageBuffer (internal)
GetMessageResults GetMessagesResult (record struct)

Performance Comparison

v1.x (OLD)

using (var hMsg = new HeapMessageArray(protocol, 10))  // ← Heap alloc
{
    hMsg.FromDataBytes(data, txFlags);  // ← Marshal + copies
    api.PTWriteMsgs(...);
}  // ← Dispose + finalizer

Allocations: HeapMessageArray object + unmanaged memory + GC pressure from finalizer

v2.0 (NEW)

_messageBuffer.WriteSingleMessage(data, txFlags);  // ← Write to pinned array
api.PTWriteMsgs(...);
// No dispose, buffer is reused

Allocations: Zero (buffer pre-allocated per channel)

What's Next

  • Port all definition enums (already good, just copy)
  • Add XML documentation
  • Unit tests
  • NuGet package
  • Benchmarks vs. v1.x

License

MIT - see LICENSE file

Product Compatible and additional computed target framework versions.
.NET net10.0-windows7.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0-windows7.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.4 110 3/13/2026
2.0.0-preview.1 47 3/13/2026