Configuration¶
All configuration lives in appsettings.json (or environment-specific overrides like appsettings.Development.json).
AMS Router¶
The embedded ADS router provides cross-platform ADS connectivity without requiring a local TwinCAT installation.
{
"AmsRouter": {
"Name": "Adsify",
"NetId": "192.168.1.78.1.1",
"TcpPort": 48898,
"LoopbackIP": "127.0.0.1",
"LoopbackPort": 48898,
"ChannelPortType": "Loopback",
"Routes": [
{
"Name": "Line1-PLC",
"Address": "192.168.10.143",
"NetId": "5.80.201.1.1.1"
}
]
}
}
Field |
Description |
|---|---|
|
Name of this ADS router instance |
|
AMS Net ID of this machine (typically |
|
ADS TCP port (default: |
|
Array of PLC routes — each needs |
Routes entries are what let the embedded router reach a remote PLC from a host with no TwinCAT installation — on Windows the OS router already holds the routes, so this is only required off-Windows. The library validates each entry at startup: NetId must be six octets in 0–255, Name and Address must be non-empty, and duplicate names fail the host.
PLC Targets¶
Maps aliases to PLC connection details. For a Real target (the default), the AmsNetId must match a route the AMS router already knows about — a Routes entry off-Windows, or a route already registered with the OS-level AMS router on Windows. A Simulated target ignores AmsNetId entirely — it never connects over the network, so there’s nothing for it to match.
{
"PlcTargets": {
"Line1": {
"AmsNetId": "5.80.201.1.1.1",
"Port": 851,
"DisplayName": "Assembly Line 1",
"TimeoutMs": 5000,
"SymbolBrowseTimeoutMs": 30000
}
}
}
Field |
Description |
Default |
|---|---|---|
|
AMS Net ID of the PLC. Required for |
(required for |
|
ADS port (851 = PLC runtime 1) |
|
|
Human-readable name shown in API responses |
|
|
Timeout for ADS operations |
|
|
Timeout for symbol browsing |
|
|
|
|
|
Seed values for a |
|
|
EtherCAT diagnostics config (see below) |
|
|
Path to a GVL_HMI v1 scenario directory (containing |
|
|
Whether the scenario’s real-time pump starts automatically once loaded. Set |
|
EtherCAT Diagnostics (per PLC)¶
Optional. When present, the EtherCAT polling service reads diagnostics for this PLC.
{
"PlcTargets": {
"Line1": {
"AmsNetId": "192.168.1.136.1.1",
"Port": 851,
"EtherCat": {
"PollingIntervalMs": 1000,
"CrcErrorThreshold": 100,
"EnableNotifications": true,
"PollCycleBudgetMs": 5000
}
}
}
}
Field |
Description |
Default |
|---|---|---|
|
Background polling interval for diagnostics |
|
|
Per-port CRC count that triggers |
|
|
Enable SignalR events for this PLC’s EtherCAT bus |
|
|
Wall-clock bound on one master’s poll cycle — a cycle running longer than this is abandoned and the master marked degraded. See the EtherCAT Diagnostics docs |
|
Feature Flags¶
Enable or disable entire feature slices (see Feature Flags (FEAT_FLAGS)). A disabled feature is excluded from the OpenAPI spec, and answers 404. The exceptions are Alarms, Rpc and TypeMetadata, which answer 501 instead, so a gateway aggregating a fleet can tell “this feature is genuinely absent” from “this one is failing right now”.
{
"Features": {
"Variables": { "Enabled": true },
"Symbols": { "Enabled": true },
"DeviceInfo": { "Enabled": true },
"Lifecycle": { "Enabled": false },
"Notifications": { "Enabled": false },
"Files": { "Enabled": false },
"EtherCatDiagnostics": { "Enabled": false },
"Mcp": { "Enabled": false },
"Simulation": { "Enabled": false }
}
}
Simulation gates the sim/* scenario control endpoints — sim/step,
sim/run, sim/pause, sim/reset, sim/state. Off by default: these endpoints drive machine
state and are for development and CI, not a shared network. Unlike the other flags above, a
disabled Simulation flag does not affect whether a Scenario loads at startup — only whether
its control endpoints are reachable.
Authentication¶
See Authentication for full details.
{
"Authentication": {
"Authority": "https://keycloak.example.com/realms/adsify",
"Audience": "adsify-api",
"RequireHttpsMetadata": true,
"RoleClaimType": "realm_access.roles",
"ValidIssuers": ["https://keycloak.example.com/realms/adsify"]
}
}
Notifications¶
{
"Notifications": {
"DefaultCycleTimeMs": 100,
"MaxSymbolsPerClient": 50,
"HeartbeatIntervalSeconds": 30
}
}
CORS¶
{
"Cors": {
"AllowedOrigins": ["https://hmi.example.com"],
"AllowCredentials": true
}
}
Key |
Default |
Effect |
|---|---|---|
|
|
The origins a browser-based client may call the API from. |
|
|
Permits credentialed cross-origin requests. Ignored when |
|
|
Serves cross-origin requests from any origin. |
An unconfigured deployment denies every cross-origin request. With AllowedOrigins empty
and AllowAnyOrigin unset, no Access-Control-Allow-Origin header is sent and a browser will
refuse to hand the response to the calling script. Both states log a warning at startup, so
check the log if a browser client is being blocked unexpectedly.
Serving any origin is a deliberate opt-in:
{
"Cors": { "AllowAnyOrigin": true }
}
Avoid it outside local development. In the Development and Simulation profiles the API also
grants full admin permissions to anonymous callers (see
Authorization), so any page a browser visits could otherwise drive a
PLC on localhost.
CORS is not an authentication control — it governs what a browser lets a script read, and has
no effect on a request from curl, a service, or any non-browser client.
Rate Limiting¶
Per-user, per-endpoint rate limiting (see Rate Limiting (FEAT_RATE_LIMITING)) protects the API from excessive requests. A global per-user limiter applies to all endpoints, and additional per-endpoint policies provide finer control.
{
"RateLimiting": {
"Enabled": true,
"GlobalPerUser": { "PermitLimit": 200, "WindowSeconds": 1 },
"VariableReads": { "PermitLimit": 100, "WindowSeconds": 1 },
"VariableWrites": { "PermitLimit": 20, "WindowSeconds": 1 },
"BatchReads": { "PermitLimit": 100, "WindowSeconds": 1 },
"BatchWrites": { "PermitLimit": 20, "WindowSeconds": 1 },
"SymbolBrowsing": { "PermitLimit": 10, "WindowSeconds": 1 },
"FileOperations": { "PermitLimit": 5, "WindowSeconds": 1 },
"NotificationConnections": { "PermitLimit": 5, "WindowSeconds": 60 }
}
}
The partition key is composed of the authenticated user ID (or client IP as fallback) combined with the plcId. When a limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header indicating when the client may retry.
Set Enabled to false to serve every request unlimited — appropriate when a gateway in front of Adsify already rate limits. The remaining keys are then ignored. One limit is not configurable and stays in force either way: PLC start/stop/reset is capped at one request per five seconds per PLC, guarding the hardware against rapid state changes.
Effective throughput¶
A batch carries up to 100 symbols, so these defaults produce the following ceilings per user, per PLC:
Path |
Endpoint |
Symbols/second |
|---|---|---|
Single read |
|
100 |
Batch read |
|
10,000 (100 requests × 100 symbols) |
Single write |
|
20 |
Batch write |
|
2,000 (20 requests × 100 symbols) |
Each batch limit deliberately matches its single-operation counterpart, so batching is never the slower choice. Reading N symbols one at a time costs N requests; batching them costs one. Equal request rates therefore make a batch no slower than a single read even at a batch of one, and N times faster at a batch of N. If you tune these values, keep BatchReads at or above VariableReads and BatchWrites at or above VariableWrites — dropping either below its counterpart penalises the cheaper path.
Note that GlobalPerUser applies on top of every policy, so no single user exceeds 200 requests/second in total regardless of the per-endpoint limits. Batching consumes that global budget far more slowly than individual reads.
These ceilings are a configuration choice, not a transport limit. A full round trip through the single-read endpoint measures roughly 175 µs on loopback, under 2% of the 10 ms budget that 100 reads/second allows; against a simulated PLC the server sustained about 6,500 reads/second on one connection and 13,000 across eight. Raise the limits if your deployment needs more.
Behaviour change. BatchReads and BatchWrites replace the former BatchOperations key, which is no longer read. A deployment that set RateLimiting:BatchOperations silently falls back to the new defaults above — a relaxation from 5 requests/second to 100 (reads) and 20 (writes). Re-apply your tuning under the new keys if you had lowered it deliberately.
Security Headers¶
Response headers applied to all API responses for defense-in-depth (see Security response headers m... (REQ_SEC_HEADERS)):
{
"SecurityHeaders": {
"StrictTransportSecurity": "max-age=31536000; includeSubDomains",
"XContentTypeOptions": "nosniff",
"XFrameOptions": "DENY",
"ContentSecurityPolicy": "default-src 'none'; frame-ancestors 'none'",
"CacheControl": "no-store",
"ReferrerPolicy": "no-referrer",
"PermissionsPolicy": "()"
}
}
Connection Limits¶
Controls the maximum number of real-time connections (SSE, WebSocket) per user:
{
"ConnectionLimits": {
"MaxSseConnectionsPerUser": 5,
"MaxWebSocketConnectionsPerUser": 5,
"MaxTotalSubscriptionsPerUser": 100,
"MaxConnectionLifetimeMinutes": 480,
"IdleTimeoutMinutes": 30
}
}
Field |
Description |
Default |
|---|---|---|
|
Maximum concurrent SSE connections per user |
|
|
Maximum concurrent WebSocket connections per user |
|
|
Maximum variable subscriptions across all connections |
|
|
Maximum lifetime of a single connection |
|
|
Disconnect after this many minutes of inactivity |
|
Symbol Access Control¶
Controls which symbols can be written per PLC (see Symbol writes must be valid... (REQ_SEC_SYMBOL_GUARD)). Configured under each PLC target:
{
"PlcTargets": {
"plc1": {
"SymbolAccess": {
"Mode": "denylist",
"Writable": [],
"Denied": []
}
}
}
}
Field |
Description |
|---|---|
|
|
|
Glob patterns for writable symbols (used in |
|
Glob patterns for denied symbols (used in |
Glob patterns: * matches one path segment, ** matches any depth. For example, MAIN.* matches MAIN.nCounter but not MAIN.stMotor.bEnabled, while MAIN.** matches both.
Value Constraints¶
Per-symbol validation rules that are enforced on writes (see Value constraints must be e... (REQ_SEC_VALUE_CONSTRAINTS)). Configured under each PLC target:
{
"PlcTargets": {
"plc1": {
"ValueConstraints": {
"MAIN.nSpeed": {
"Min": 0,
"Max": 100
},
"MAIN.sMode": {
"Enum": [1, 2, 3]
},
"MAIN.sName": {
"Pattern": "^[A-Z].*"
}
}
}
}
}
Constraint |
Description |
|---|---|
|
Numeric range validation. The write is rejected if the value falls outside the range. |
|
Discrete allowed values. The write is rejected if the value is not in the list. |
|
Regular expression for string validation. The write is rejected if the value does not match. |
|
When |
Writes that violate constraints return 400 with error code VALUE_OUT_OF_RANGE.