|
| 1 | +# ADS Integration Architecture |
| 2 | + |
| 3 | +## Decision |
| 4 | + |
| 5 | +**Use Beckhoff.TwinCAT.Ads for direct ADS communication** instead of a custom ADS-over-MQTT relay protocol. |
| 6 | + |
| 7 | +### Context |
| 8 | + |
| 9 | +The original architecture assumed a custom relay where the monitor server would exchange ADS read/write commands as MQTT messages (`flowforge/ads/read/*`, `flowforge/ads/write/*`, `flowforge/ads/notification/*`). Research revealed this is unnecessary: |
| 10 | + |
| 11 | +- **ADS-over-MQTT is a native TwinCAT router feature** — transparent to application code. Once configured on the PLC via `TcConfig.xml`, any `AdsClient` connects normally via `AmsNetId`. |
| 12 | +- **`Beckhoff.TwinCAT.Ads.TcpRouter`** provides a software ADS router for non-TwinCAT systems (Linux Docker containers). |
| 13 | +- MQTT remains for **FlowForge internal messaging** (build notifications, progress updates) but is no longer used for ADS relay. |
| 14 | + |
| 15 | +### Consequences |
| 16 | + |
| 17 | +| Component | Before | After | |
| 18 | +|-----------|--------|-------| |
| 19 | +| **Monitor Server** | MQTT relay topics for ADS reads | `Beckhoff.TwinCAT.Ads` + `TcpRouter` for direct ADS-over-TCP | |
| 20 | +| **Build Server** | MQTT relay for deploy commands | `Beckhoff.TwinCAT.Ads` natively (Windows/TwinCAT router) | |
| 21 | +| **Shared MQTT Topics** | `flowforge/ads/read/*`, `write/*`, `notification/*` | Removed — MQTT for build notifications only | |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## NuGet Packages |
| 26 | + |
| 27 | +| Package | Version | Used By | Purpose | |
| 28 | +|---------|---------|---------|---------| |
| 29 | +| `Beckhoff.TwinCAT.Ads` | 7.0.* | Monitor Server, Build Server | Core ADS client (`AdsClient`) | |
| 30 | +| `Beckhoff.TwinCAT.Ads.TcpRouter` | 7.0.* | Monitor Server only | Software ADS router for Linux/Docker | |
| 31 | + |
| 32 | +Both packages target .NET 8.0, .NET 10.0, and .NET Standard 2.0. They work with .NET 9.0 via the .NET Standard 2.0 target. |
| 33 | + |
| 34 | +--- |
| 35 | + |
| 36 | +## Key API Patterns |
| 37 | + |
| 38 | +### Connection |
| 39 | + |
| 40 | +```csharp |
| 41 | +// On Windows with TwinCAT installed (build server): |
| 42 | +var client = new AdsClient(); |
| 43 | +client.Connect(AmsNetId.Parse("192.168.1.100.1.1"), 851); |
| 44 | + |
| 45 | +// On Linux/Docker with TcpRouter (monitor server): |
| 46 | +// TcpRouter must be started first, then AdsClient connects normally. |
| 47 | +``` |
| 48 | + |
| 49 | +**Port 851** = PLC Runtime 1 (default). Ports 852, 853 for additional runtimes. |
| 50 | + |
| 51 | +### Variable Access |
| 52 | + |
| 53 | +**Symbol-based read** (dynamic, for discovery): |
| 54 | +```csharp |
| 55 | +var loader = SymbolLoaderFactory.Create(client, settings); |
| 56 | +var value = loader.Symbols["MAIN.nCounter"].ReadValue(); |
| 57 | +``` |
| 58 | + |
| 59 | +**Handle-based read** (faster for repeated access): |
| 60 | +```csharp |
| 61 | +uint handle = client.CreateVariableHandle("MAIN.nCounter"); |
| 62 | +int value = (int)client.ReadAny(handle, typeof(int)); |
| 63 | +client.DeleteVariableHandle(handle); |
| 64 | +``` |
| 65 | + |
| 66 | +**Sum Commands** (batch — critical for performance): |
| 67 | +- 4000 individual reads = 4–8 seconds |
| 68 | +- 4000 reads via Sum Command = ~10 ms |
| 69 | +- Max 500 sub-commands per call |
| 70 | + |
| 71 | +### Notifications (Monitor Server) |
| 72 | + |
| 73 | +```csharp |
| 74 | +client.AddDeviceNotificationEx( |
| 75 | + "MAIN.nCounter", |
| 76 | + AdsTransMode.OnChange, |
| 77 | + cycleTime: 100, // ms — check interval |
| 78 | + maxDelay: 0, // ms — max delay before notification |
| 79 | + userData: null, |
| 80 | + type: typeof(int)); |
| 81 | +``` |
| 82 | + |
| 83 | +- **Max 1024 notifications per connection** |
| 84 | +- Notifications fire on background threads |
| 85 | +- Always unregister when done (`DeleteDeviceNotification`) |
| 86 | + |
| 87 | +### PLC State Management (Build Server — Deploy) |
| 88 | + |
| 89 | +```csharp |
| 90 | +// Read state |
| 91 | +StateInfo state = client.ReadState(); |
| 92 | +// state.AdsState == AdsState.Run / Stop / Config / etc. |
| 93 | +
|
| 94 | +// Switch to config mode (required before activation) |
| 95 | +client.WriteControl(new StateInfo(AdsState.Reconfig, 0)); |
| 96 | + |
| 97 | +// Restart to run mode |
| 98 | +client.WriteControl(new StateInfo(AdsState.Run, 0)); |
| 99 | +``` |
| 100 | + |
| 101 | +--- |
| 102 | + |
| 103 | +## PlcAdsState Enum |
| 104 | + |
| 105 | +Mirrored in `FlowForge.Shared.Models.Ads.PlcAdsState` (no Beckhoff dependency in Shared): |
| 106 | + |
| 107 | +| Value | Name | FlowForge Meaning | |
| 108 | +|-------|------|--------------------| |
| 109 | +| 5 | **Run** | PLC running — deploy needs approval if production target | |
| 110 | +| 6 | **Stop** | PLC stopped — safe for deploy | |
| 111 | +| 11 | **Error** | PLC error — needs investigation | |
| 112 | +| 15 | **Config** | Config mode — safe for deploy | |
| 113 | +| 16 | **Reconfig** | Transitioning to config mode | |
| 114 | + |
| 115 | +Deploy lock logic: `IsSafeForDeploy = State is Stop or Config`. |
| 116 | + |
| 117 | +--- |
| 118 | + |
| 119 | +## Component Architecture |
| 120 | + |
| 121 | +### Monitor Server (Linux/Docker) |
| 122 | + |
| 123 | +``` |
| 124 | +┌─────────────────────────────────┐ |
| 125 | +│ Monitor Container │ |
| 126 | +│ │ |
| 127 | +│ ┌──────────────────────────┐ │ |
| 128 | +│ │ IAdsClient │ │ ADS-over-TCP |
| 129 | +│ │ (AdsClientWrapper) │───────────────────────► PLC |
| 130 | +│ │ Uses: AdsClient + │ │ Port 48898 |
| 131 | +│ │ TcpRouter │ │ |
| 132 | +│ └──────────┬───────────────┘ │ |
| 133 | +│ │ │ |
| 134 | +│ ┌──────────▼───────────────┐ │ |
| 135 | +│ │ SubscriptionManager │ │ |
| 136 | +│ └──────────┬───────────────┘ │ |
| 137 | +│ │ │ |
| 138 | +│ ┌──────────▼───────────────┐ │ SignalR |
| 139 | +│ │ PlcDataHub (SignalR) │◄─────────────────── Frontend |
| 140 | +│ └──────────────────────────┘ │ |
| 141 | +└─────────────────────────────────┘ |
| 142 | +``` |
| 143 | + |
| 144 | +- Each container gets a unique local `AmsNetId` (derived from IP or session ID). |
| 145 | +- `TcpRouter` establishes the ADS-over-TCP connection to the target PLC. |
| 146 | +- `AdsClient` connects through the local `TcpRouter`. |
| 147 | + |
| 148 | +### Build Server (Windows/TwinCAT) |
| 149 | + |
| 150 | +``` |
| 151 | +┌─────────────────────────────────┐ |
| 152 | +│ Build Server (Windows) │ |
| 153 | +│ │ |
| 154 | +│ ┌──────────────────────────┐ │ |
| 155 | +│ │ IAdsDeployClient │ │ Native ADS |
| 156 | +│ │ (AdsDeployClient) │───────────────────────► PLC |
| 157 | +│ │ Uses: AdsClient │ │ (via TwinCAT router) |
| 158 | +│ └──────────────────────────┘ │ |
| 159 | +│ │ |
| 160 | +│ ┌──────────────────────────┐ │ |
| 161 | +│ │ IAutomationInterface │ │ COM Interop |
| 162 | +│ │ (ActivateConfiguration) │───────────────────────► TwinCAT XAE |
| 163 | +│ └──────────────────────────┘ │ |
| 164 | +└─────────────────────────────────┘ |
| 165 | +``` |
| 166 | + |
| 167 | +- No `TcpRouter` needed — uses the native TwinCAT router on Windows. |
| 168 | +- Deploy sequence: connect → read state → switch to config → activate → restart → verify. |
| 169 | + |
| 170 | +--- |
| 171 | + |
| 172 | +## Deploy Sequence (Build Server) |
| 173 | + |
| 174 | +1. **Connect** to target PLC via ADS (`IAdsDeployClient.ConnectAsync`) |
| 175 | +2. **Read PLC state** — deploy lock check (`ReadPlcStateAsync`) |
| 176 | +3. If running + production → require 4-eyes approval (handled by backend before queuing) |
| 177 | +4. **Switch to config mode** (`SwitchToConfigModeAsync` → `AdsState.Reconfig`) |
| 178 | +5. **Activate configuration** via Automation Interface (`IAutomationInterface.ActivateConfiguration`) |
| 179 | +6. **Start/restart TwinCAT** via ADS (`StartRestartTwinCatAsync` → `AdsState.Run`) |
| 180 | +7. **Verify** PLC is in Run state |
| 181 | +8. **Disconnect** |
| 182 | + |
| 183 | +--- |
| 184 | + |
| 185 | +## MQTT Topic Changes |
| 186 | + |
| 187 | +### Removed |
| 188 | +- `flowforge/ads/read/{amsNetId}` — replaced by direct ADS reads |
| 189 | +- `flowforge/ads/write/{amsNetId}` — replaced by direct ADS writes |
| 190 | +- `flowforge/ads/notification/{amsNetId}` — replaced by ADS notifications |
| 191 | + |
| 192 | +### Retained |
| 193 | +- `flowforge/build/notify/{twincat-version}` — backend → build servers (wake-up signal) |
| 194 | +- `flowforge/build/progress/{build-id}` — build server → backend (progress updates) |
| 195 | + |
| 196 | +### Added |
| 197 | +- `flowforge/deploy/status/{deploy-id}` — build server → backend (deploy progress) |
| 198 | + |
| 199 | +--- |
| 200 | + |
| 201 | +## References |
| 202 | + |
| 203 | +- [Beckhoff.TwinCAT.Ads NuGet](https://www.nuget.org/packages/Beckhoff.TwinCAT.Ads) |
| 204 | +- [Beckhoff.TwinCAT.Ads.TcpRouter NuGet](https://www.nuget.org/packages/Beckhoff.TwinCAT.Ads.TcpRouter/) |
| 205 | +- [ADS-over-MQTT Manual](https://download.beckhoff.com/download/document/automation/twincat3/ADS-over-MQTT_en.pdf) |
| 206 | +- [Beckhoff/ADS-over-MQTT_Samples](https://github.com/Beckhoff/ADS-over-MQTT_Samples) |
| 207 | +- [Beckhoff/TF6000_ADS_DOTNET_V5_Samples](https://github.com/Beckhoff/TF6000_ADS_DOTNET_V5_Samples) |
| 208 | +- [ADS Notifications](https://infosys.beckhoff.com/content/1033/tc3_adsnetref/7312578699.html) |
| 209 | +- [ADS Sum Commands](https://infosys.beckhoff.com/content/1033/tc3_adssamples_net/185258507.html) |
| 210 | +- [AdsState Enum](https://infosys.beckhoff.com/content/1033/tc3_adsnetref/7313023115.html) |
| 211 | +- [ITcSysManager.ActivateConfiguration](https://infosys.beckhoff.com/content/1033/tc3_automationinterface/242759819.html) |
| 212 | +- [Secure ADS](https://download.beckhoff.com/download/document/automation/twincat3/Secure_ADS_EN.pdf) |
0 commit comments