Skip to content
Embedded ADS Router

Embedded ADS Router

Adsify’s ADS connectivity — the embedded router, per-target connection pooling, and automatic reconnection — is provided by the Dahlke.TwinCAT.Ads NuGet package, not by adsify’s own code. Program.cs wires it in with a single call:

builder.Services.AddTwinCatAds(builder.Configuration);

This registers the router, the connection factory, and the connection pool, binding the same AmsRouter and PlcTargets configuration sections described in Configuration. Adsify’s own code only ever depends on the package’s public surface — chiefly IAdsConnectionPool and IAdsConnection — never on the concrete types behind it, most of which are internal to the package.

Embedded Router

The package embeds an ADS TCP/IP router built on Beckhoff’s TwinCAT.Ads.TcpRouter, eliminating the dependency on a locally installed TwinCAT AMS router and enabling cross-platform operation on Linux, macOS, and Windows.

Internally, a BackgroundService starts the embedded router and resolves a one-shot readiness signal once the router reports RouterStatus.Started. Both of those types are internal to the package — they exist purely to coordinate the connection pool’s own startup and are not part of adsify’s or the package’s public API.

The router is skipped entirely when there is nothing for it to route. It never starts — and the readiness signal resolves immediately — when either:

  • every configured PLC target has Mode: "Simulated" (no target ever touches the network), or
  • AmsRouter:NetId is not configured (the host is expected to have a system TwinCAT router already running).

This is the mechanism the whole Simulation profile depends on: appsettings.Simulation.json configures only a Mode: "Simulated" target, so the embedded router never starts and the API runs with no TwinCAT installation, no hardware, and no AMS router at all.

Connection Pool

The connection pool creates one stable connection facade per configured PLC target — not one per connection. That facade (an internal implementation of the public IAdsConnection interface) is handed out for the lifetime of the pool and its identity never changes; the actual ADS/AMS session underneath it is torn down and rebuilt by a per-target reconnection loop whenever it drops. Callers never see the swap: every operation routes through whichever connection is current, and an operation issued while a target has none waits — up to that target’s TimeoutMs — for the reconnect loop to publish a new one before failing.

This is also why subscriptions now survive a reconnect. A subscription registered through the facade is kept as a durable record; when the pool publishes a freshly connected connection, the facade automatically re-registers every active subscription against it. Callers hold a single IDisposable for the life of the subscription — it does not need to be recreated after a reconnection.

Startup for real (non-simulated) targets is deferred behind the router’s readiness signal so that connection attempts never race the router; simulated targets connect immediately regardless, since they never touch the network or wait on the router. Reconnection uses exponential backoff — starting at 2 seconds and doubling up to a 30-second cap — with a health check every 5 seconds while connected.

Graceful Degradation

If a target has no live connection — the router failed to start, the PLC is unreachable, or a reconnect is still in progress — operations against that target wait out TimeoutMs and then throw AdsConnectionUnavailableException, which AdsExceptionMiddleware maps to 503 PLC_UNAVAILABLE (see Error Handling) instead of crashing the API.