Skip to content

Commit bb2caa1

Browse files
q-soriartyclaude
andauthored
feat(config): add shared ADS types and integration architecture doc (#113)
Add PlcAdsState enum, PlcStateDto, AdsConnectionInfo, and AdsVariableSubscription to FlowForge.Shared for use across monitor and build server without requiring Beckhoff NuGet dependencies. Update PlcTargetDto with CurrentState and DeployRequestDto with AdsPort. Create doc/ADS_INTEGRATION.md documenting the decision to use direct Beckhoff.TwinCAT.Ads instead of custom MQTT ADS relay topics. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b784148 commit bb2caa1

File tree

7 files changed

+291
-0
lines changed

7 files changed

+291
-0
lines changed

doc/ADS_INTEGRATION.md

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila)
2+
// SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
namespace FlowForge.Shared.Models.Ads;
5+
6+
public record AdsConnectionInfo
7+
{
8+
public string AmsNetId { get; init; } = string.Empty;
9+
public int AdsPort { get; init; } = 851;
10+
11+
/// <summary>
12+
/// Hostname or IP of the target PLC. Used by TcpRouter on non-TwinCAT
13+
/// systems (e.g. Linux Docker containers) to establish ADS-over-TCP.
14+
/// </summary>
15+
public string? TargetHostname { get; init; }
16+
17+
/// <summary>TCP port for the ADS router on the target (default 48898).</summary>
18+
public int TcpPort { get; init; } = 48898;
19+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila)
2+
// SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
namespace FlowForge.Shared.Models.Ads;
5+
6+
public record AdsVariableSubscription
7+
{
8+
public string VariablePath { get; init; } = string.Empty;
9+
public int CycleTimeMs { get; init; } = 100;
10+
public int MaxDelayMs { get; init; }
11+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila)
2+
// SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
namespace FlowForge.Shared.Models.Ads;
5+
6+
/// <summary>
7+
/// Mirrors TwinCAT.Ads.AdsState for use in shared DTOs without requiring a
8+
/// Beckhoff NuGet dependency in the Shared library.
9+
/// See: https://infosys.beckhoff.com/content/1033/tc3_adsnetref/7313023115.html
10+
/// </summary>
11+
public enum PlcAdsState
12+
{
13+
Invalid = 0,
14+
Idle = 1,
15+
Reset = 2,
16+
Init = 3,
17+
Start = 4,
18+
Run = 5,
19+
Stop = 6,
20+
SaveConfig = 7,
21+
LoadConfig = 8,
22+
PowerFailure = 9,
23+
PowerGood = 10,
24+
Error = 11,
25+
Shutdown = 12,
26+
Suspend = 13,
27+
Resume = 14,
28+
Config = 15,
29+
Reconfig = 16
30+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila)
2+
// SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
namespace FlowForge.Shared.Models.Ads;
5+
6+
public record PlcStateDto
7+
{
8+
public string AmsNetId { get; init; } = string.Empty;
9+
public PlcAdsState State { get; init; }
10+
public DateTimeOffset Timestamp { get; init; }
11+
12+
public bool IsRunning => State == PlcAdsState.Run;
13+
public bool IsInConfigMode => State is PlcAdsState.Config or PlcAdsState.Reconfig;
14+
public bool IsSafeForDeploy => State is PlcAdsState.Stop or PlcAdsState.Config;
15+
}

src/shared/FlowForge.Shared/Models/Deploy/DeployRequestDto.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ public record DeployRequestDto
88
public Guid ProjectId { get; init; }
99
public string TargetAmsNetId { get; init; } = string.Empty;
1010
public string? ApproverId { get; init; }
11+
public int AdsPort { get; init; } = 851;
1112
}

src/shared/FlowForge.Shared/Models/Target/PlcTargetDto.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila)
22
// SPDX-License-Identifier: AGPL-3.0-or-later
33

4+
using FlowForge.Shared.Models.Ads;
5+
46
namespace FlowForge.Shared.Models.Target;
57

68
public record PlcTargetDto
@@ -13,4 +15,5 @@ public record PlcTargetDto
1315
public Guid? GroupId { get; init; }
1416
public bool IsProductionTarget { get; init; }
1517
public bool DeployLocked { get; init; }
18+
public PlcAdsState? CurrentState { get; init; }
1619
}

0 commit comments

Comments
 (0)