Skip to content

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"
      }
    ]
  }
}
FieldDescription
NameName of this ADS router instance
NetIdAMS Net ID of this machine (typically <your-ip>.1.1)
TcpPortADS TCP port (default: 48898)
RoutesArray of PLC routes — each needs Name, Address (IP), and NetId

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
    }
  }
}
FieldDescriptionDefault
AmsNetIdAMS Net ID of the PLC. Required for Real targets; ignored for Simulated targets.(required for Real)
PortADS port (851 = PLC runtime 1)851
DisplayNameHuman-readable name shown in API responses""
TimeoutMsTimeout for ADS operations5000
SymbolBrowseTimeoutMsTimeout for symbol browsing30000
ModeReal (connect over ADS/AMS) or Simulated (in-memory value store, no PLC or router needed)Real
InitialValuesSeed values for a Simulated target, keyed by symbol path. Ignored for Real targets. See the simulated quick start for the caveat that JSON-bound seed values read back as strings. Not replaced by Scenario below — the two are independent seed sources layered on top of each other; see Scenario.{}
EtherCatEtherCAT diagnostics config (see below)null (disabled)
ScenarioPath to a GVL_HMI v1 scenario directory (containing machine.yaml), resolved relative to the application content root; absolute paths work too. Only valid on a Simulated target — startup fails if declared on a Real one. See Simulation.null (no scenario)
ScenarioAutoStartWhether the scenario’s real-time pump starts automatically once loaded. Set false for deterministic testing: the target seeds but does not tick, so a test drives it entirely through POST sim/step.true

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
      }
    }
  }
}
FieldDescriptionDefault
PollingIntervalMsBackground polling interval for diagnostics1000
CrcErrorThresholdPer-port CRC count that triggers CrcErrorThresholdExceededEvent notification100
EnableNotificationsEnable SignalR events for this PLC’s EtherCAT bustrue
PollCycleBudgetMsWall-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 docs5000

Feature Flags

Enable or disable entire feature slices. 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 endpointssim/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
  }
}
KeyDefaultEffect
AllowedOrigins[]The origins a browser-based client may call the API from.
AllowCredentialsfalsePermits credentialed cross-origin requests. Ignored when AllowAnyOrigin is set — the CORS protocol forbids that combination.
AllowAnyOriginfalseServes 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 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:

PathEndpointSymbols/second
Single readGET /variables/{symbolPath}100
Batch readGET /variables?paths=…10,000 (100 requests × 100 symbols)
Single writePUT /variables/{symbolPath}20
Batch writePUT /variables2,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:

{
  "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
  }
}
FieldDescriptionDefault
MaxSseConnectionsPerUserMaximum concurrent SSE connections per user5
MaxWebSocketConnectionsPerUserMaximum concurrent WebSocket connections per user5
MaxTotalSubscriptionsPerUserMaximum variable subscriptions across all connections100
MaxConnectionLifetimeMinutesMaximum lifetime of a single connection480
IdleTimeoutMinutesDisconnect after this many minutes of inactivity30

Symbol Access Control

Controls which symbols can be written per PLC. Configured under each PLC target:

{
  "PlcTargets": {
    "plc1": {
      "SymbolAccess": {
        "Mode": "denylist",
        "Writable": [],
        "Denied": []
      }
    }
  }
}
FieldDescription
Modeallowlist — only symbols matching Writable patterns can be written. denylist — all symbols can be written except those matching Denied patterns.
WritableGlob patterns for writable symbols (used in allowlist mode)
DeniedGlob patterns for denied symbols (used in denylist mode)

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. 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].*"
        }
      }
    }
  }
}
ConstraintDescription
Min / MaxNumeric range validation. The write is rejected if the value falls outside the range.
EnumDiscrete allowed values. The write is rejected if the value is not in the list.
PatternRegular expression for string validation. The write is rejected if the value does not match.
ReadOnlyWhen true, all writes to this symbol are rejected.

Writes that violate constraints return 400 with error code VALUE_OUT_OF_RANGE.