A comprehensive, easy-to-use C# .NET library for CANopen file formats: CiA DS 306 (EDS, DCF, CPJ) and CiA 311 (XDD, XDC).
EdsDcfNet exposes the CANopen Object Dictionary as a fully typed, editable API. Applications can inspect, query, create, and configure objects and sub-objects directly; the library is not limited to file conversion or lossless round-trips.
✨ Simple API - Intuitive, fluent API style for quick integration
🗂️ First-Class Object Dictionary API - Query, create, and modify CANopen objects, sub-objects, object lists, data types, access rights, defaults, and configured values
📖 Read & Write EDS - Parse and generate Electronic Data Sheets
📝 Read & Write DCF - Process and create Device Configuration Files
🌐 Read & Write CPJ - Parse and create Nodelist Project files (CiA 306-3 network topologies)
🧩 Read & Write XDD/XDC - Parse and generate CiA 311 XML device descriptions/configurations
🔄 EDS to DCF Conversion - Easy conversion with configuration parameters
🎯 Type-Safe - Fully typed models for all CANopen objects
📦 Modular - Support for modular devices (bus couplers + modules)
✅ CiA DS 306 v1.4 / CiA 311 v1.1 Compliant - Implemented according to official specification
using EdsDcfNet;
// Read EDS file
var eds = CanOpenFile.Eds.ReadFile("device.eds");
// Display device information
Console.WriteLine($"Device: {eds.DeviceInfo.ProductName}");
Console.WriteLine($"Vendor: {eds.DeviceInfo.VendorName}");
Console.WriteLine($"Product Number: 0x{eds.DeviceInfo.ProductNumber:X}");Every EDS, DCF, XDD, and XDC model exposes its parsed object dictionary through
ObjectDictionary. It is an application-facing model, not merely parser state: use it
to inspect device capabilities, find objects and sub-objects by index, evaluate default
or configured values, update a DCF configuration, or build an object dictionary in code.
using EdsDcfNet;
using EdsDcfNet.Extensions;
using EdsDcfNet.Models;
var dcf = CanOpenFile.Dcf.ReadFile("configured_device.dcf");
var dictionary = dcf.ObjectDictionary;
// Query objects and sub-objects by their CANopen index.
var deviceType = dictionary.GetObject(0x1000);
var mappingEntry = dictionary.GetSubObject(0x1A00, 0x01);
Console.WriteLine(deviceType?.ParameterName);
// Read values as object or as a strongly typed value. The .NET type is derived
// automatically from DataType in the Object Dictionary.
object? rawDeviceType = dictionary.GetParameterValueAsObject(0x1000); // uint for UNSIGNED32
uint mapping = dictionary.GetParameterValue<uint>(0x1A00, 0x01);
// Pass a .NET value directly. It is validated against the OD data type and converted
// to the textual ParameterValue representation persisted by the writer.
if (!dictionary.SetParameterValue(0x1A00, 0x01, 0x60000108U))
throw new InvalidOperationException("TPDO mapping entry 0x1A00:01 is missing.");
// Create a manufacturer-specific object programmatically.
// ObjectType/DataType take the CiA 301 constants instead of magic numbers.
dictionary.ManufacturerObjects.Add(0x2000);
dictionary.Objects[0x2000] = new CanOpenObject
{
Index = 0x2000,
ParameterName = "Application mode",
ObjectType = CanOpenObjectType.Var,
DataType = CanOpenDataType.Unsigned8,
AccessType = AccessType.ReadWrite,
DefaultValue = "0",
ParameterValue = "1",
PdoMapping = true
};
CanOpenFile.Dcf.WriteFile(dcf, "configured_device_updated.dcf");The constants also carry metadata (bit length, signedness, display name) — see Data-type metadata below.
The model distinguishes mandatory, optional, and manufacturer-specific object lists and
represents ARRAY and RECORD entries through typed CanOpenSubObject instances. Convenience
extensions also provide category queries and access to RPDO/TPDO communication and mapping
parameter ranges.
Typed value conversion covers BOOLEAN, signed and unsigned integers (including the CANopen
24/40/48/56-bit types), REAL32/REAL64, VISIBLE_STRING, UNICODE_STRING, and OCTET_STRING.
Numeric ranges are validated when setting values, and REAL32/REAL64 values must be finite:
NaN and values that overflow the target range (for example double.MaxValue on a REAL32
entry) are rejected instead of being written as Infinity. DOMAIN entries are excluded
because DCF files reference their payload through UploadFile/DownloadFile instead of an
inline value; use those properties directly. The original string overloads
remain available when an application needs exact control of the serialized representation.
using EdsDcfNet;
var eds = CanOpenFile.Eds.ReadFile("device.eds");
eds.FileInfo.FileRevision++;
CanOpenFile.Eds.WriteFile(eds, "device_updated.eds");using EdsDcfNet;
using System.Threading;
using var cts = new CancellationTokenSource();
var eds = await CanOpenFile.Eds.ReadFileAsync("device.eds", cancellationToken: cts.Token);
eds.FileInfo.FileRevision++;
await CanOpenFile.Eds.WriteFileAsync(eds, "device_updated.eds", cancellationToken: cts.Token);using EdsDcfNet;
using System.IO;
using var stream = File.OpenRead("device.eds");
var eds = CanOpenFile.Eds.ReadStream(stream);
using var outStream = new MemoryStream();
CanOpenFile.Eds.WriteStream(eds, outStream);Stream ownership: stream overloads do not dispose input/output streams.
The caller remains responsible for stream lifetime.
For new code, use the format-specific entry points on CanOpenFile instead of the
legacy static Read* / Write* overloads:
| Format | Entry point | Example |
|---|---|---|
| EDS | CanOpenFile.Eds |
CanOpenFile.Eds.ReadFile("device.eds") |
| DCF | CanOpenFile.Dcf |
CanOpenFile.Dcf.WriteFile(dcf, "out.dcf") |
| CPJ | CanOpenFile.Cpj |
CanOpenFile.Cpj.ReadFile("network.cpj") |
| XDD | CanOpenFile.Xdd |
CanOpenFile.Xdd.ReadFile("device.xdd") |
| XDC | CanOpenFile.Xdc |
CanOpenFile.Xdc.ReadFile("device.xdc") |
These entry points accept CanOpenFileOptions (MaxInputSize, StrictParsing)
and CanOpenWriteOptions (pre-write validation) in one place. Legacy facade
Read* / Write* overloads (path, stream, string, sync, async, and options-taking
variants) remain for backward compatibility and are marked [Obsolete] (advisory);
they will be removed in a future major release.
EDS-to-DCF conversion lives on the EDS entry point: CanOpenFile.Eds.ConvertToDcf(...).
The no-timestamp CanOpenFile.EdsToDcf(...) overload is obsolete and delegates
there; the EdsToDcf(..., DateTime timestamp, ...) overload is intentionally
not obsolete (thin retained shim for deterministic timestamps) but new code
should still call Eds.ConvertToDcf.
using EdsDcfNet;
var eds = CanOpenFile.Eds.ReadFile("device.eds");
var dcf = CanOpenFile.Eds.ConvertToDcf(eds, nodeId: 2, baudrate: 500);
CanOpenFile.Dcf.WriteFile(dcf, "device_node2.dcf", CanOpenWriteOptions.Validated);If your code still calls the legacy CanOpenFile.Read* / Write* / EdsToDcf static
methods, move to the format entry points in the table above. All legacy
Read* / Write* facade overloads are marked [Obsolete] (advisory)—not only
default-parameter variants—and remain available until a future major release.
Each format uses the same method names on its entry point (Eds, Dcf, Cpj, Xdd,
Xdc). Replace the legacy facade prefix with the matching entry point:
| Legacy facade method | Canonical replacement |
|---|---|
ReadEds(...), ReadDcf(...), … |
Eds.ReadFile(...), Dcf.ReadFile(...), … |
ReadEdsFromString(...), … |
Eds.ReadString(...), Dcf.ReadString(...), … |
ReadEds(stream, ...), … |
Eds.ReadStream(stream, ...), Dcf.ReadStream(stream, ...), … |
ReadEdsAsync(path, ...), … |
Eds.ReadFileAsync(path, ...), Dcf.ReadFileAsync(path, ...), … |
ReadEdsAsync(stream, ...), … |
Eds.ReadStreamAsync(stream, ...), … |
WriteEds(...), … |
Eds.WriteFile(...), Dcf.WriteFile(...), … |
WriteEds(model, stream), … |
Eds.WriteStream(model, stream), … |
WriteEdsAsync(...), … |
Eds.WriteFileAsync(...), Eds.WriteStreamAsync(...), … |
WriteEdsToString(...), … |
Eds.WriteToString(...), Dcf.WriteToString(...), … |
EdsToDcf(...) (no timestamp) |
Eds.ConvertToDcf(...) — obsolete |
EdsToDcf(..., DateTime timestamp, ...) |
Prefer Eds.ConvertToDcf(..., timestamp, ...); facade overload kept (not obsolete) |
These CanOpenFile static members are not marked [Obsolete]:
| Member | Notes |
|---|---|
Validate(...) / ValidateAsync(...) |
Unchanged validation entry points |
EnsureValid(...) / EnsureValidAsync(...) |
Throw-on-invalid helpers for EDS/DCF/CPJ |
EdsToDcf(..., DateTime timestamp, ...) |
Intentional retained shim; prefer Eds.ConvertToDcf for new code |
Format entry points (Eds, Dcf, Cpj, Xdd, Xdc) |
Canonical API |
Pass CanOpenFileOptions instead of a bare maxInputSize parameter:
// Before
var xdd = CanOpenFile.ReadXdd("device.xdd", maxInputSize: 50L * 1024 * 1024);
// After
var xdd = CanOpenFile.Xdd.ReadFile(
"device.xdd",
new CanOpenFileOptions { MaxInputSize = 50L * 1024 * 1024 });Use CanOpenWriteOptions.Validated on the format entry point write methods (see
Validating models before write operations).
// Before
var dcf = CanOpenFile.EdsToDcf(eds, nodeId: 2, baudrate: 500);
// After
var dcf = CanOpenFile.Eds.ConvertToDcf(eds, nodeId: 2, baudrate: 500);For deterministic generated timestamps (recommended in tests and reproducible builds),
pass an explicit DateTime to ConvertToDcf:
var dcf = CanOpenFile.Eds.ConvertToDcf(
eds, nodeId: 2, timestamp: DateTime.UtcNow, baudrate: 500);// Before
var eds = CanOpenFile.ReadEds("device.eds");
var dcf = CanOpenFile.EdsToDcf(eds, nodeId: 2, baudrate: 500);
CanOpenFile.WriteDcf(dcf, "device_node2.dcf");
// After
var eds = CanOpenFile.Eds.ReadFile("device.eds");
var dcf = CanOpenFile.Eds.ConvertToDcf(eds, nodeId: 2, baudrate: 500);
CanOpenFile.Dcf.WriteFile(dcf, "device_node2.dcf");All writer APIs that persist text (CanOpenFile.Eds, .Dcf, .Cpj, .Xdd, and .Xdc
write methods) write UTF-8 without BOM by default for file and stream output.
This is an intentional interoperability choice:
- CiA DS 306 is historically ASCII-oriented.
- UTF-8 keeps full ASCII compatibility for 7-bit content.
- UTF-8 also preserves non-ASCII characters in names/comments instead of replacing them.
If a downstream tool only accepts strict ASCII, keep model text in 7-bit ASCII characters, or transcode explicitly to strict ASCII at your boundary and fail fast on non-ASCII content.
using EdsDcfNet;
using System.IO;
using System.Text;
var asciiStrict = Encoding.GetEncoding(
"us-ascii",
EncoderFallback.ExceptionFallback,
DecoderFallback.ExceptionFallback);
var dcf = CanOpenFile.Dcf.ReadFile("device.dcf");
var text = CanOpenFile.Dcf.WriteToString(dcf);
File.WriteAllText("device_ascii.dcf", text, asciiStrict);using EdsDcfNet;
// Read XDD file
var xdd = CanOpenFile.Xdd.ReadFile("device.xdd");
Console.WriteLine($"Device: {xdd.DeviceInfo.ProductName}");
Console.WriteLine($"Vendor: {xdd.DeviceInfo.VendorName}");using EdsDcfNet;
// Read DCF file
var dcf = CanOpenFile.Dcf.ReadFile("configured_device.dcf");
Console.WriteLine($"Node ID: {dcf.DeviceCommissioning.NodeId}");
Console.WriteLine($"Baudrate: {dcf.DeviceCommissioning.Baudrate} kbit/s");using EdsDcfNet;
// Read XDC file
var xdc = CanOpenFile.Xdc.ReadFile("configured_device.xdc");
Console.WriteLine($"Node ID: {xdc.DeviceCommissioning.NodeId}");
Console.WriteLine($"Baudrate: {xdc.DeviceCommissioning.Baudrate} kbit/s");XDD/XDC files may include an ApplicationProcess element describing device parameters
at the application level. The typed model gives full programmatic access to all
sub-constructs.
using EdsDcfNet;
var xdd = CanOpenFile.Xdd.ReadFile("device.xdd");
if (xdd.ApplicationProcess is { } ap)
{
// Iterate parameters
foreach (var param in ap.ParameterList)
{
var displayName = param.LabelGroup.GetDisplayName() ?? param.UniqueId;
Console.WriteLine($"Parameter: {displayName}");
}
// Inspect data type definitions
if (ap.DataTypeList is { } dtl)
{
foreach (var enumType in dtl.Enums)
Console.WriteLine($"Enum type: {enumType.Name}");
}
}using EdsDcfNet;
// Read EDS
var eds = CanOpenFile.Eds.ReadFile("device.eds");
// Convert to DCF with node ID and baudrate
var dcf = CanOpenFile.Eds.ConvertToDcf(eds, nodeId: 2, baudrate: 500, nodeName: "MyDevice");
// Save DCF
CanOpenFile.Dcf.WriteFile(dcf, "device_node2.dcf");Use the validation API to detect invalid commissioning values and inconsistent object-list definitions before serializing files.
using EdsDcfNet;
using EdsDcfNet.Validation;
var dcf = CanOpenFile.Dcf.ReadFile("configured_device.dcf");
IReadOnlyList<ValidationIssue> issues = CanOpenFile.Validate(dcf);
if (issues.Count > 0)
{
foreach (var issue in issues)
Console.WriteLine(issue);
}CanOpenFile.Validate(...) is the recommended entry point and routes to the
full model validator, returning path-based ValidationIssue entries.
Current checks include:
- commissioning constraints (Node-ID range
1..127for commissioned nodes;NodeId == 0is accepted only when commissioning is omitted, baudrate range with0accepted for that omitted state, key string limits) - device info constraints (name/order-code length, granularity limit)
- object dictionary consistency (list membership, duplicates, missing entries)
- object-level constraints (object type validity, parameter-name length, SubNumber mismatch)
The CiA 306 Node-ID range used by these checks is exposed publicly via
CanOpenNodeId, so consumers can validate or document node IDs without
duplicating the 1..127 literals:
using EdsDcfNet;
bool valid = CanOpenNodeId.IsInRange(nodeId); // true for 1..127
byte min = CanOpenNodeId.MinValue; // 1
byte max = CanOpenNodeId.MaxValue; // 127
string range = CanOpenNodeId.RangeDescription; // "1..127"To validate automatically before writing, pass CanOpenWriteOptions.Validated to the
format-specific entry points:
using EdsDcfNet;
var dcf = CanOpenFile.Dcf.ReadFile("configured_device.dcf");
// Throws ModelValidationException when the model has validation issues.
CanOpenFile.Dcf.WriteFile(dcf, "updated.dcf", CanOpenWriteOptions.Validated);The same option works on CanOpenFile.Eds, .Cpj, .Xdd, and .Xdc write methods.
Legacy CanOpenFile.WriteDcf(...) overloads delegate to these entry points.
For very large models, use the async validation API so validation runs on a thread-pool thread with cooperative cancellation instead of blocking the caller:
using EdsDcfNet;
using EdsDcfNet.Validation;
IReadOnlyList<ValidationIssue> issues = await CanOpenFile.ValidateAsync(dcf, cancellationToken);
// Or throw ModelValidationException on issues:
await CanOpenFile.EnsureValidAsync(dcf, cancellationToken);ValidateAsync / EnsureValidAsync exist for EDS, DCF, and CPJ models. The
cancellation token is observed at iteration boundaries (per object-dictionary
entry, per network node), so validation of large models can be cancelled
mid-run.
Async write methods with CanOpenWriteOptions.Validated use this path
automatically — validation is awaited and honors the write call's
CancellationToken:
await CanOpenFile.Dcf.WriteFileAsync(dcf, "updated.dcf", CanOpenWriteOptions.Validated, cancellationToken);Synchronous write methods keep the existing synchronous validation behavior.
using EdsDcfNet;
using EdsDcfNet.Models;
// Read a CPJ file describing the network topology
var cpj = CanOpenFile.Cpj.ReadFile("nodelist.cpj");
foreach (var network in cpj.Networks)
{
Console.WriteLine($"Network: {network.NetName}");
foreach (var node in network.Nodes.Values)
{
Console.WriteLine($" Node {node.NodeId}: {node.Name} ({node.DcfFileName})");
}
}
// Create a new CPJ
var project = new NodelistProject();
project.Networks.Add(new NetworkTopology
{
NetName = "Production Line 1",
Nodes =
{
[2] = new NetworkNode { NodeId = 2, Present = true, Name = "PLC", DcfFileName = "plc.dcf" },
[3] = new NetworkNode { NodeId = 3, Present = true, Name = "IO Module", DcfFileName = "io.dcf" }
}
});
CanOpenFile.Cpj.WriteFile(project, "network.cpj");using EdsDcfNet.Extensions;
var dcf = CanOpenFile.Dcf.ReadFile("device.dcf");
// Get object
var deviceType = dcf.ObjectDictionary.GetObject(0x1000);
// Set value (returns true if object exists, false if not found)
bool set = dcf.ObjectDictionary.SetParameterValue(0x1000, "0x00000191");
// Browse PDO objects
var tpdos = dcf.ObjectDictionary.GetPdoCommunicationParameters(transmit: true);Writer encoding note: all file/stream write methods on the format entry points use UTF-8 without BOM.
Each format exposes read/write operations via a static property (Eds, Dcf, Cpj, Xdd, Xdc).
The shared surface on every format entry point includes:
// Read (file, string, stream; sync and async)
TModel ReadFile(string filePath, CanOpenFileOptions? options = null)
Task<TModel> ReadFileAsync(string filePath, CanOpenFileOptions? options = null, CancellationToken cancellationToken = default)
TModel ReadString(string content, CanOpenFileOptions? options = null)
TModel ReadStream(Stream stream, CanOpenFileOptions? options = null)
Task<TModel> ReadStreamAsync(Stream stream, CanOpenFileOptions? options = null, CancellationToken cancellationToken = default)
// Write (file, stream, string; sync and async; optional CanOpenWriteOptions)
void WriteFile(TModel model, string filePath)
void WriteFile(TModel model, string filePath, CanOpenWriteOptions? options)
void WriteStream(TModel model, Stream stream)
void WriteStream(TModel model, Stream stream, CanOpenWriteOptions? options)
Task WriteFileAsync(TModel model, string filePath, CancellationToken cancellationToken = default)
Task WriteFileAsync(TModel model, string filePath, CanOpenWriteOptions? options, CancellationToken cancellationToken = default)
Task WriteStreamAsync(TModel model, Stream stream, CancellationToken cancellationToken = default)
Task WriteStreamAsync(TModel model, Stream stream, CanOpenWriteOptions? options, CancellationToken cancellationToken = default)
string WriteToString(TModel model)Format-specific model types:
| Entry point | Read/write model |
|---|---|
CanOpenFile.Eds |
ElectronicDataSheet |
CanOpenFile.Dcf |
DeviceConfigurationFile |
CanOpenFile.Cpj |
NodelistProject |
CanOpenFile.Xdd |
ElectronicDataSheet |
CanOpenFile.Xdc |
DeviceConfigurationFile |
EDS-to-DCF conversion:
DeviceConfigurationFile ConvertToDcf(ElectronicDataSheet eds, byte nodeId,
ushort baudrate = 250, string? nodeName = null)Model validation:
IReadOnlyList<ValidationIssue> Validate(ElectronicDataSheet eds)
IReadOnlyList<ValidationIssue> Validate(DeviceConfigurationFile dcf)Legacy static Read* / Write* facade methods remain for backward compatibility
and are all marked [Obsolete] (advisory). The no-timestamp EdsToDcf overload is
obsolete; EdsToDcf(..., DateTime timestamp, ...) is retained without [Obsolete]
as a thin shim (prefer Eds.ConvertToDcf). Validate* is unchanged.
All read APIs apply a safe default input-size limit of 10 MB
(IniParser.DefaultMaxInputSize) to reduce denial-of-service risk from
unexpectedly large payloads.
You can override this limit per operation when you need to process larger files:
var xdd = CanOpenFile.Xdd.ReadFile(
"large-device.xdd",
new CanOpenFileOptions { MaxInputSize = 50L * 1024 * 1024 });Set CanOpenFileOptions.StrictParsing = true so silent read coercions fail with
EdsParseException instead of mapping to defaults. Default remains lenient.
This applies to reads through the CanOpenFile format entry points (and legacy
facade overloads that accept options). Direct *Reader APIs without options
stay lenient (no public way to enable StrictParsing on those readers).
Today this covers:
- Duplicate keys within an INI section
- Unknown XDD/XDC baud-rate strings (
supportedBaudRate,actualBaudRate,baudRate/@defaultValue) - Unknown boolean tokens (
ValueConverter.ParseBoolean) and CPJ present-flag tokens (ValueConverter.ParsePresentFlag) - Unknown access-type tokens (
ValueConverter.ParseAccessTypeand XDDParseXddAccessType) and unknown XDD XML bools (ParseXmlBool) - EDS/DCF
FileVersion/FileRevisionand XDD/XDCfileVersionmajor/minor tooling forms (1.0/1,0); zero-padded values such as010parse as decimal10across EDS/DCF/XDD - Missing XDD/XDC
indexonCANopenObject, and missing or invalidobjectType(schema-valid unsignedByte forms such as+9/-0are accepted after trim; missingCANopenSubObjectsubIndexstays lenient) - Malformed XDD/XDC unsigned numeric attributes (
objFlags,subNumber,pDOmappingIndex, general-feature counts,networkNumber)
var eds = CanOpenFile.Eds.ReadFile(
"device.eds",
new CanOpenFileOptions { StrictParsing = true });Guidance:
- Keep the default whenever possible.
- Enable StrictParsing for trusted inputs when you want malformed tokens to fail fast instead of coercing.
- Increase
MaxInputSizeonly for trusted sources and known use cases. - Set the limit just high enough for your expected maximum file size.
Between lenient (silently coerce) and strict (throw on the first deviation)
there is a third path: the Read*WithDiagnostics methods on the format entry
points return the parsed model and report every repair as a
ParseDiagnostic — for import UIs and device-file validators that need to
answer "what did the parser silently fix?" without giving up the model.
var result = CanOpenFile.Eds.ReadFileWithDiagnostics("device.eds");
foreach (var diagnostic in result.Diagnostics)
Console.WriteLine(diagnostic); // [Warning] INI_DUPLICATE_KEY at FileInfo.FileName:42: ...
var model = result.Model; // fully parsed, coercions appliedEach ParseDiagnostic carries:
Code— stable machine-readable identifier (ParseDiagnosticCodes, e.g.INI_DUPLICATE_KEY,XDD_MISSING_INDEX). In strict mode the same condition throws anEdsParseExceptionwhoseCodeproperty carries the same value.Severity(Info/Warning/Error),Message,Path(section/key for INI formats, XPath-like for XML formats),Line(INI formats),RawValue, andCoercedTo(what lenient mode substituted).
Diagnostics are collected through an AsyncLocal sink scoped to the call, so
concurrent reads do not interfere. Direct *Reader APIs stay lenient and
silent — collection happens only through the CanOpenFile entry points.
- Entry points are safe for concurrent use.
CanOpenFile.Eds/.Dcf/.Cpj/.Xdd/.Xdcand theirRead*/Write*/Validateoperations may be called from multiple threads or async flows simultaneously. The operation objects behind them are stateless singletons whose delegates construct a fresh reader/writer per call, andStrictParsingstate is scoped per call viaAsyncLocal, so concurrent calls with different options do not interfere. This contract is guarded by a concurrency test (tests/EdsDcfNet.Tests/Integration/ThreadSafetyTests.cs). - Models are not thread-safe.
ElectronicDataSheet,DeviceConfigurationFile,NodelistProject,ObjectDictionary, etc. are plain mutable objects. Do not mutate a model while it is being written, validated, or converted (EdsToDcf/ConvertToDcf); give each thread its own model instance. - Caller-owned streams and files are not synchronized. The
ReadStream*/WriteStream*overloads operate directly on theStreamyou pass, and the file-based overloads contend on the external file system. Concurrent calls must each use their own stream and target distinct paths — sharing one stream races its position, and concurrent writes to the same path (or a read overlapping a write) can throw a sharingIOExceptionor expose truncated content. - Options may be shared.
CanOpenFileOptionsandCanOpenWriteOptionsare immutable (init-only); a single instance can be reused across threads.
CanOpenFileOptions (read) and CanOpenWriteOptions (write) are intentionally
small, shared across all formats, and hold only cross-format concerns
(MaxInputSize, StrictParsing, and pre-write ValidateBeforeWrite).
When a genuinely format-specific option becomes necessary (for example XDD XML formatting, INI section ordering, or CPJ network defaults), the agreed extension pattern is derived per-format option types, not new properties on the shared types:
// Pattern (illustrative — implemented only when a concrete option exists):
public class XddWriteOptions : CanOpenWriteOptions
{
public bool IndentXml { get; init; } = true;
}
CanOpenFile.Xdd.WriteFile(xdd, "device.xdd", new XddWriteOptions { IndentXml = false });Rules for adding such an option:
- The shared base types stay limited to cross-format concerns; unrelated
format-specific properties must not accumulate on them (IntelliSense on
CanOpenFile.Edsshould never show XDD-only options). - The base types are unsealed on demand in the same PR that introduces the first derived type (unsealing is a non-breaking, additive change).
- The derived type flows through the existing
CanOpenWriteOptions?/CanOpenFileOptions?parameters; the format-specific writer/reader checks for its own derived type. Existing signatures, overload shapes, and parameter names are untouched (see the Public API compatibility checklist in CONTRIBUTING.md). - No format-specific option type is added before a concrete requirement exists.
CanOpenDataType exposes CiA 301 (§7.4.7) data-type index constants and
lookup helpers for the raw ushort values stored in
CanOpenObject.DataType / CanOpenSubObject.DataType:
using EdsDcfNet;
using EdsDcfNet.Extensions;
var eds = CanOpenFile.Eds.ReadFile("device.eds");
var dictionary = eds.ObjectDictionary;
ushort dataType = dictionary.GetObject(0x1000)?.DataType ?? 0; // Device Type: UNSIGNED32
CanOpenDataType.IsStandardType(dataType); // true
CanOpenDataType.TryGetBitLength(dataType); // 32; null for variable-length types
CanOpenDataType.IsSigned(dataType); // false
CanOpenDataType.IsUnsigned(dataType); // true
CanOpenDataType.GetName(dataType); // "UNSIGNED32", or null when unknownTryGetBitLength is the single source of truth for fixed bit widths and backs
CanOpenValueConverter's own conversion widths, so consumers get the same
answer the library uses internally. It returns null for variable-length
types (VISIBLE_STRING, OCTET_STRING, UNICODE_STRING, DOMAIN), reserved
codes, and manufacturer-specific or unknown types (0x0040 and above) — do not
assume a fixed width when the result is null.
- ✅ First-class, editable CANopen Object Dictionary model for EDS, DCF, XDD, and XDC
- ✅ Indexed lookup, creation, and modification of objects and sub-objects
- ✅ Mandatory, optional, and manufacturer-specific object lists
- ✅ Default values and DCF/XDC configured parameter values
- ✅ Automatic conversion between OD data types and .NET values, with range validation
- ✅ CiA 301 data-type metadata lookup (bit length, signedness, display name via
CanOpenDataType) - ✅ Helpers for RPDO/TPDO communication and mapping parameters
- ✅ Complete EDS parsing and writing
- ✅ Complete DCF parsing and writing
- ✅ CPJ nodelist project parsing and writing (CiA 306-3 network topologies)
- ✅ XDD parsing and writing (CiA 311 XML device description)
- ✅ XDC parsing and writing (CiA 311 XML device configuration)
- ✅ All Object Types (NULL, DOMAIN, DEFTYPE, DEFSTRUCT, VAR, ARRAY, RECORD)
- ✅ Sub-objects and sub-indexes
- ✅ Compact Storage (CompactSubObj, CompactPDO)
- ✅ Object Links
- ✅ Modular device concept
- ✅ Hexadecimal (
0x), decimal, and octal (0+digit, e.g.010→ 8 — not padded decimal) - ✅ $NODEID formula evaluation (e.g., $NODEID+0x200)
- ✅ CANopen Safety (EN 50325-5) - SRDOMapping, InvertedSRAD
- ✅ Comments and additional sections
Writer APIs expose format-specific exceptions with context:
EdsWriter/CanOpenFile.Edswrite methods:EdsWriteExceptionDcfWriter/CanOpenFile.Dcfwrite methods:DcfWriteExceptionCpjWriter/CanOpenFile.Cpjwrite methods:CpjWriteExceptionXddWriter/CanOpenFile.Xddwrite methods:XddWriteExceptionXdcWriter/CanOpenFile.Xdcwrite methods:XdcWriteException
When a failure can be attributed to a concrete generated section/element,
the exception contains a SectionName value (for example DeviceInfo,
Topology, DeviceProfile, or deviceCommissioning).
Complete examples can be found in the examples/EdsDcfNet.Examples project.
A dedicated BenchmarkDotNet project is available at:
benchmarks/EdsDcfNet.Benchmarks
Run all benchmarks:
dotnet run -c Release -p benchmarks/EdsDcfNet.Benchmarks -- --filter "*"Baseline scenario definitions and artifact locations are documented in:
benchmarks/EdsDcfNet.Benchmarks/BASELINE.md
eds-dcf-net/
├── src/
│ └── EdsDcfNet/ # Main library
│ ├── Models/ # Data models
│ ├── Parsers/ # EDS/DCF/CPJ/XDD/XDC parsers
│ ├── Writers/ # EDS/DCF/CPJ/XDD/XDC writers
│ ├── Utilities/ # Helper classes
│ ├── Exceptions/ # Custom exceptions
│ └── Extensions/ # Extension methods
├── benchmarks/
│ └── EdsDcfNet.Benchmarks/ # BenchmarkDotNet throughput/memory benchmarks
├── examples/
│ └── EdsDcfNet.Examples/ # Example application
└── docs/
├── architecture/ # ARC42 software architecture
└── cia/ # CiA DS 306 specification
For consuming the NuGet package:
- Any .NET implementation compatible with .NET Standard 2.0 (e.g., .NET Framework 4.6.1+, .NET Core 2.0+, .NET 5+, Unity, Xamarin)
Strong naming: EdsDcfNet.dll is strong-named as of 1.13.0. The key
(src/EdsDcfNet/EdsDcfNet.snk) is committed and public — it provides assembly
identity only, not authenticity. On .NET (Core) 5+ nothing changes; .NET
Framework consumers that referenced the previously unsigned assembly must
rebuild against 1.13.0 (no source changes required).
For building this repository (library, tests, examples):
- .NET SDK 10.0 or higher
- C# 13.0 (as provided by the .NET 10 SDK)
MIT License - see LICENSE file
Based on:
- CiA DS 306 Version 1.4.0 (December 15, 2021)
- CiA 311 XML device description/configuration concepts (XDD/XDC)
For questions or issues:
- GitHub Issues: https://github.com/dborgards/eds-dcf-net/issues
EdsDcfNet - Professional CANopen EDS/DCF/CPJ/XDD/XDC processing in C# .NET