Dahlke.EtherCAT.Esi
0.11.0
dotnet add package Dahlke.EtherCAT.Esi --version 0.11.0
NuGet\Install-Package Dahlke.EtherCAT.Esi -Version 0.11.0
<PackageReference Include="Dahlke.EtherCAT.Esi" Version="0.11.0" />
<PackageVersion Include="Dahlke.EtherCAT.Esi" Version="0.11.0" />
<PackageReference Include="Dahlke.EtherCAT.Esi" />
paket add Dahlke.EtherCAT.Esi --version 0.11.0
#r "nuget: Dahlke.EtherCAT.Esi, 0.11.0"
#:package Dahlke.EtherCAT.Esi@0.11.0
#addin nuget:?package=Dahlke.EtherCAT.Esi&version=0.11.0
#tool nuget:?package=Dahlke.EtherCAT.Esi&version=0.11.0
Dahlke.EtherCAT.Esi
An EtherCAT Slave Information (ESI) device catalogue. It parses vendor ESI XML and answers one question: given a vendor / product / revision triple read off a live bus, which device description is this?
No ADS, no TwinCAT, no Beckhoff dependency. This package is XML, options and logging. That independence is deliberate — it is why the catalogue is separate from Dahlke.EtherCAT.Diagnostics, which is the package that talks to a master over ADS. Use this one alone if all you have is a folder of ESI files.
dotnet add package Dahlke.EtherCAT.Esi
Quick start
Point it at a directory of ESI XML and resolve an identity:
builder.Services.AddEsiCatalog(builder.Configuration.GetSection("Esi"));
{
"Esi": {
"Directory": "C:/TwinCAT/3.1/Config/Io/EtherCAT",
"LookupBudgetMs": 5000
}
}
public sealed class SlaveNamer(IEsiCatalog catalog)
{
public async Task<string> DescribeAsync(uint vendorId, uint productCode, uint revision)
{
var result = await catalog.LookupAsync(
new EsiKey(vendorId, productCode, revision),
typeHint: "EL3204");
return result.Status switch
{
EsiStatus.Resolved => result.Device!.NameEn ?? "unnamed device",
EsiStatus.NotFound => "unknown device",
_ => $"lookup failed: {result.Status}",
};
}
}
EsiStatus separates a resolved device from one that could not be resolved and why: NotConfigured when no ESI directory is set or it does not exist, NotFound when nothing matched, and further members for an identity that was never scanned or a directory that could not be read. So a caller can tell "I have no ESI folder configured" from "I looked and it is not there".
One case deliberately does not get its own status: a lookup that exhausts LookupBudgetMs reports NotFound, the same as genuine absence. It is logged at warning with the budget that was hit, so it is diagnosable — but if your code needs to distinguish "not on the bus" from "I stopped looking", watch the log rather than the status.
What it does for you
Ranks candidates by the type hint. A real ESI folder holds hundreds of files and the sought identity is in one of them. The hint (typically the slave's type string, e.g. EL3204) orders the search so the likely file is opened first, rather than parsing the folder alphabetically.
The file name is read as the pattern Beckhoff writes, with an x standing where a digit varies — so ELx9xx scores against EL1904 as though it were EL19xx. That is what keeps the cross-family files reachable: every TwinSAFE I/O terminal (EL1904, EL2904, EK1914, EP1908) is filed under ELx9xx / EKx9xx / EPx9xx rather than the EL19xx its model number suggests. A wildcard stands in for a digit only, so the literal series letter of ELXxxxx is not mistaken for one, and ties break toward the name spelling out the most leading digits — EL1904 still tries EL19xx first, and only the miss falls through to ELx9xx.
Ranking decides only the order files are opened; the identity match alone decides the answer, so a hint that ranks badly costs time, never correctness. The case that still gets no help is a non-Beckhoff vendor, whose inferred type is Unknown or Vendor(0x…) and prefixes no real file name — that lookup falls back to scanning the directory alphabetically, which is the whole-set worst case LookupBudgetMs exists to bound.
Bounds the work. LookupBudgetMs caps a single lookup. The budget is checked between files, not once up front, so a large folder cannot be turned into an unbounded scan by one unlucky query — the lookup gives up and warns rather than blocking a request thread.
Parses each device at most once per process, and complains at most once per device. Both are properties of a single shared instance, which is why AddEsiCatalog registers IEsiCatalog as a singleton. Any other lifetime silently loses them.
Tolerates a bad folder. A malformed or unreadable ESI file does not fail the lookup that happened to reach it; it is logged once and skipped, so one corrupt vendor file cannot take out the catalogue.
Reports the declared E-bus current. EsiDevice.EBusCurrentMa carries what
<Info><Electrical><EBusCurrent> states, in mA, with ESI's own sign convention: positive draws
from the E-bus, negative supplies it. An EL3201 declares 190; an EK1100 coupler declares
-500, matching its own name, "EK1100 EtherCAT Coupler (0.5A E-Bus)".
It is null when the file declares none, and never 0 for that case — 478 devices in
Beckhoff's published set declare a genuine 0, so summing draws across a segment would otherwise
be unable to tell an unknown contributor from one that draws nothing. Aggregating per-device
figures into a segment load against a segment budget is yours to do; this reports the figure.
Reports the declared CoE object dictionary. EsiDevice.ObjectDictionary carries what
<Profile><Dictionary> declares — per object its index, name, data type, bit size, access flags
and default — which is the metadata a live SDO upload does not carry. Look one up without scanning:
if (device.ObjectDictionary?.TryGetObject(0x1018, out var identity) == true)
{
Console.WriteLine(identity.Name); // "Identity"
Console.WriteLine(identity.SubItems[1].Name); // "Vendor ID"
Console.WriteLine(identity.SubItems[1].SubIndex); // 1 -> render as 0x1018:01
}
Record sub-items are nested under their parent, not flattened. SubIndex is nullable because ESI
genuinely omits it for array members, whose indices are implied by <ArrayInfo> rather than
stated — deriving one would be inference, so absence is reported as absence.
null means the device declares no dictionary. A device declaring an empty one reports a
non-null value with an empty Objects — a different answer, deliberately.
Reports the declared process-data map. EsiDevice.ProcessData answers "what does this slave
put on and take off the wire, through which sync manager", entirely offline:
foreach (var pdo in device.ProcessData?.Pdos ?? [])
{
// Direction is slave-relative: Transmit is the slave transmitting,
// which is the master's process INPUTS.
Console.WriteLine($"{pdo.Direction} 0x{pdo.Index:X4} {pdo.Name} -> Sm {pdo.SyncManager}");
}
SyncManager is nullable and null is the majority case — 37,541 of 58,128 PDOs in Beckhoff's
published set declare no Sm. Padding entries (index 0, a bit length, nothing else) are
reported rather than filtered, because dropping them corrupts any bit-offset arithmetic a
consumer does over the entries.
Unlike the object dictionary above, ESI gives process data no container element — <Sm>,
<TxPdo> and <RxPdo> are direct children of <Device> — so a device with no sync managers and
no PDOs is indistinguishable from one declaring an empty map, and both report null.
Registration is not eager
AddEsiCatalog does not resolve the catalogue. Whether a misconfigured ESI directory should be reported at startup or on first use is a hosting decision, so it is left to you:
var app = builder.Build();
app.Services.GetRequiredService<IEsiCatalog>(); // fail at startup instead
Links
- Source, issues and the other packages in this repository: https://github.com/patdhlk/Dahlke.TwinCAT.Ads
- Changelog: https://github.com/patdhlk/Dahlke.TwinCAT.Ads/blob/main/CHANGELOG.md
Apache-2.0.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Bcl.TimeProvider (>= 8.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
- System.Collections.Immutable (>= 8.0.0)
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Options (>= 9.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 9.0.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Dahlke.EtherCAT.Esi:
| Package | Downloads |
|---|---|
|
Dahlke.EtherCAT.Diagnostics
EtherCAT master and slave diagnostics over TwinCAT ADS — topology scanning, slave and port state, CRC and frame error counters, sync-unit faults, and a change-event stream, read through raw ADS index groups rather than the symbol API. |
|
|
OpenEC.Monitor
Passive EtherCAT monitoring SDK: EtherCAT frame decoding, pcap/pcapng reading, ENI/ESI parsing, and ENI-independent bus learning for .NET. |
GitHub repositories
This package is not used by any popular GitHub repositories.