diff --git a/doc/ADS_INTEGRATION.md b/doc/ADS_INTEGRATION.md new file mode 100644 index 0000000..f0b6c9a --- /dev/null +++ b/doc/ADS_INTEGRATION.md @@ -0,0 +1,212 @@ +# ADS Integration Architecture + +## Decision + +**Use Beckhoff.TwinCAT.Ads for direct ADS communication** instead of a custom ADS-over-MQTT relay protocol. + +### Context + +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: + +- **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`. +- **`Beckhoff.TwinCAT.Ads.TcpRouter`** provides a software ADS router for non-TwinCAT systems (Linux Docker containers). +- MQTT remains for **FlowForge internal messaging** (build notifications, progress updates) but is no longer used for ADS relay. + +### Consequences + +| Component | Before | After | +|-----------|--------|-------| +| **Monitor Server** | MQTT relay topics for ADS reads | `Beckhoff.TwinCAT.Ads` + `TcpRouter` for direct ADS-over-TCP | +| **Build Server** | MQTT relay for deploy commands | `Beckhoff.TwinCAT.Ads` natively (Windows/TwinCAT router) | +| **Shared MQTT Topics** | `flowforge/ads/read/*`, `write/*`, `notification/*` | Removed — MQTT for build notifications only | + +--- + +## NuGet Packages + +| Package | Version | Used By | Purpose | +|---------|---------|---------|---------| +| `Beckhoff.TwinCAT.Ads` | 7.0.* | Monitor Server, Build Server | Core ADS client (`AdsClient`) | +| `Beckhoff.TwinCAT.Ads.TcpRouter` | 7.0.* | Monitor Server only | Software ADS router for Linux/Docker | + +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. + +--- + +## Key API Patterns + +### Connection + +```csharp +// On Windows with TwinCAT installed (build server): +var client = new AdsClient(); +client.Connect(AmsNetId.Parse("192.168.1.100.1.1"), 851); + +// On Linux/Docker with TcpRouter (monitor server): +// TcpRouter must be started first, then AdsClient connects normally. +``` + +**Port 851** = PLC Runtime 1 (default). Ports 852, 853 for additional runtimes. + +### Variable Access + +**Symbol-based read** (dynamic, for discovery): +```csharp +var loader = SymbolLoaderFactory.Create(client, settings); +var value = loader.Symbols["MAIN.nCounter"].ReadValue(); +``` + +**Handle-based read** (faster for repeated access): +```csharp +uint handle = client.CreateVariableHandle("MAIN.nCounter"); +int value = (int)client.ReadAny(handle, typeof(int)); +client.DeleteVariableHandle(handle); +``` + +**Sum Commands** (batch — critical for performance): +- 4000 individual reads = 4–8 seconds +- 4000 reads via Sum Command = ~10 ms +- Max 500 sub-commands per call + +### Notifications (Monitor Server) + +```csharp +client.AddDeviceNotificationEx( + "MAIN.nCounter", + AdsTransMode.OnChange, + cycleTime: 100, // ms — check interval + maxDelay: 0, // ms — max delay before notification + userData: null, + type: typeof(int)); +``` + +- **Max 1024 notifications per connection** +- Notifications fire on background threads +- Always unregister when done (`DeleteDeviceNotification`) + +### PLC State Management (Build Server — Deploy) + +```csharp +// Read state +StateInfo state = client.ReadState(); +// state.AdsState == AdsState.Run / Stop / Config / etc. + +// Switch to config mode (required before activation) +client.WriteControl(new StateInfo(AdsState.Reconfig, 0)); + +// Restart to run mode +client.WriteControl(new StateInfo(AdsState.Run, 0)); +``` + +--- + +## PlcAdsState Enum + +Mirrored in `FlowForge.Shared.Models.Ads.PlcAdsState` (no Beckhoff dependency in Shared): + +| Value | Name | FlowForge Meaning | +|-------|------|--------------------| +| 5 | **Run** | PLC running — deploy needs approval if production target | +| 6 | **Stop** | PLC stopped — safe for deploy | +| 11 | **Error** | PLC error — needs investigation | +| 15 | **Config** | Config mode — safe for deploy | +| 16 | **Reconfig** | Transitioning to config mode | + +Deploy lock logic: `IsSafeForDeploy = State is Stop or Config`. + +--- + +## Component Architecture + +### Monitor Server (Linux/Docker) + +``` +┌─────────────────────────────────┐ +│ Monitor Container │ +│ │ +│ ┌──────────────────────────┐ │ +│ │ IAdsClient │ │ ADS-over-TCP +│ │ (AdsClientWrapper) │───────────────────────► PLC +│ │ Uses: AdsClient + │ │ Port 48898 +│ │ TcpRouter │ │ +│ └──────────┬───────────────┘ │ +│ │ │ +│ ┌──────────▼───────────────┐ │ +│ │ SubscriptionManager │ │ +│ └──────────┬───────────────┘ │ +│ │ │ +│ ┌──────────▼───────────────┐ │ SignalR +│ │ PlcDataHub (SignalR) │◄─────────────────── Frontend +│ └──────────────────────────┘ │ +└─────────────────────────────────┘ +``` + +- Each container gets a unique local `AmsNetId` (derived from IP or session ID). +- `TcpRouter` establishes the ADS-over-TCP connection to the target PLC. +- `AdsClient` connects through the local `TcpRouter`. + +### Build Server (Windows/TwinCAT) + +``` +┌─────────────────────────────────┐ +│ Build Server (Windows) │ +│ │ +│ ┌──────────────────────────┐ │ +│ │ IAdsDeployClient │ │ Native ADS +│ │ (AdsDeployClient) │───────────────────────► PLC +│ │ Uses: AdsClient │ │ (via TwinCAT router) +│ └──────────────────────────┘ │ +│ │ +│ ┌──────────────────────────┐ │ +│ │ IAutomationInterface │ │ COM Interop +│ │ (ActivateConfiguration) │───────────────────────► TwinCAT XAE +│ └──────────────────────────┘ │ +└─────────────────────────────────┘ +``` + +- No `TcpRouter` needed — uses the native TwinCAT router on Windows. +- Deploy sequence: connect → read state → switch to config → activate → restart → verify. + +--- + +## Deploy Sequence (Build Server) + +1. **Connect** to target PLC via ADS (`IAdsDeployClient.ConnectAsync`) +2. **Read PLC state** — deploy lock check (`ReadPlcStateAsync`) +3. If running + production → require 4-eyes approval (handled by backend before queuing) +4. **Switch to config mode** (`SwitchToConfigModeAsync` → `AdsState.Reconfig`) +5. **Activate configuration** via Automation Interface (`IAutomationInterface.ActivateConfiguration`) +6. **Start/restart TwinCAT** via ADS (`StartRestartTwinCatAsync` → `AdsState.Run`) +7. **Verify** PLC is in Run state +8. **Disconnect** + +--- + +## MQTT Topic Changes + +### Removed +- `flowforge/ads/read/{amsNetId}` — replaced by direct ADS reads +- `flowforge/ads/write/{amsNetId}` — replaced by direct ADS writes +- `flowforge/ads/notification/{amsNetId}` — replaced by ADS notifications + +### Retained +- `flowforge/build/notify/{twincat-version}` — backend → build servers (wake-up signal) +- `flowforge/build/progress/{build-id}` — build server → backend (progress updates) + +### Added +- `flowforge/deploy/status/{deploy-id}` — build server → backend (deploy progress) + +--- + +## References + +- [Beckhoff.TwinCAT.Ads NuGet](https://www.nuget.org/packages/Beckhoff.TwinCAT.Ads) +- [Beckhoff.TwinCAT.Ads.TcpRouter NuGet](https://www.nuget.org/packages/Beckhoff.TwinCAT.Ads.TcpRouter/) +- [ADS-over-MQTT Manual](https://download.beckhoff.com/download/document/automation/twincat3/ADS-over-MQTT_en.pdf) +- [Beckhoff/ADS-over-MQTT_Samples](https://github.com/Beckhoff/ADS-over-MQTT_Samples) +- [Beckhoff/TF6000_ADS_DOTNET_V5_Samples](https://github.com/Beckhoff/TF6000_ADS_DOTNET_V5_Samples) +- [ADS Notifications](https://infosys.beckhoff.com/content/1033/tc3_adsnetref/7312578699.html) +- [ADS Sum Commands](https://infosys.beckhoff.com/content/1033/tc3_adssamples_net/185258507.html) +- [AdsState Enum](https://infosys.beckhoff.com/content/1033/tc3_adsnetref/7313023115.html) +- [ITcSysManager.ActivateConfiguration](https://infosys.beckhoff.com/content/1033/tc3_automationinterface/242759819.html) +- [Secure ADS](https://download.beckhoff.com/download/document/automation/twincat3/Secure_ADS_EN.pdf) diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index 9b8acdf..ffd4a4a 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -16,10 +16,10 @@ FlowForge is a visual PLC programming platform: - **Backend → MQTT Broker**: Build notify (wake up build servers) - **Backend → Monitor Container**: Container lifecycle management (start/stop on demand) - **Build Server → Backend**: REST (poll for work, report results) -- **Build Server ↔ MQTT Broker**: Receive build notify, send progress updates, deploy commands (ADS over MQTT) +- **Build Server ↔ MQTT Broker**: Receive build notify, send progress/deploy status updates - **Build Server ↔ GitHub**: Git clone/fetch + push generated PLC solution -- **Monitor Container ↔ MQTT Broker**: ADS over MQTT reads/subscriptions for live PLC data -- **Build Server → MQTT Broker → PLC**: Deploy/activate via ADS over MQTT (requires TwinCAT Engineering on the build server) +- **Build Server → PLC**: Deploy/activate via direct ADS (Beckhoff.TwinCAT.Ads, native TwinCAT router) +- **Monitor Container → PLC**: Live PLC data via direct ADS-over-TCP (Beckhoff.TwinCAT.Ads + TcpRouter) ## User Workflow @@ -59,7 +59,7 @@ FlowForge is a visual PLC programming platform: 2. Backend spins up a dedicated monitor container for that session 3. Backend issues a short-lived auth token and returns the container's SignalR endpoint to the frontend 4. Frontend connects directly to the monitor container via SignalR -5. Monitor container reads PLC data via ADS over MQTT and streams it to the frontend in real-time +5. Monitor container reads PLC data via direct ADS (Beckhoff.TwinCAT.Ads + TcpRouter) and streams it to the frontend in real-time 6. User closes the monitoring view → backend stops the container ## Components @@ -120,9 +120,9 @@ FlowForge is a visual PLC programming platform: - Create TwinCAT project structure - Commit/push generated PLC solution back to the repo - Interface with Beckhoff Automation Interface for compilation -- Deploy (activate) TwinCAT solution on target PLC via ADS over MQTT (requires TwinCAT Engineering) +- Deploy (activate) TwinCAT solution on target PLC via direct ADS (requires TwinCAT Engineering) -**Note**: Build and Deploy are separate operations with a shared permission hierarchy. The build server handles both: generation/compilation (build) and PLC activation via ADS over MQTT (deploy). Deploy requires TwinCAT Engineering, which is only available on the build server. The backend orchestrates the requests; the build server executes them. +**Note**: Build and Deploy are separate operations with a shared permission hierarchy. The build server handles both: generation/compilation (build) and PLC activation via direct ADS (deploy). Deploy requires TwinCAT Engineering, which is only available on the build server. The backend orchestrates the requests; the build server executes them. **Technology**: C# (.NET) - Required for Automation Interface compatibility @@ -135,7 +135,7 @@ FlowForge is a visual PLC programming platform: **Architecture**: Single project with organized folders (Hubs, Auth, Services). See [MODULE_ARCHITECTURE.md](MODULE_ARCHITECTURE.md). **Key Responsibilities**: -- Read PLC variables via ADS over MQTT (cyclic reads or event-based subscriptions) +- Read PLC variables via direct ADS (Beckhoff.TwinCAT.Ads + TcpRouter; cyclic reads or event-based subscriptions) - Convert ADS data to frontend-consumable format - Stream live data directly to the frontend via SignalR - Authenticate frontend connections using short-lived tokens issued by the backend @@ -144,11 +144,11 @@ FlowForge is a visual PLC programming platform: **Why separate from the backend?** - Monitoring is long-lived and resource-intensive (continuous ADS reads); the backend API is short-lived request-response -- The ADS data is already on the MQTT broker — routing it back through MQTT just to proxy via the backend is an unnecessary round-trip +- Direct ADS-over-TCP from the container to the PLC avoids unnecessary MQTT round-trips - Isolation: a slow or crashing PLC connection does not affect the backend API - Scalability: containers scale independently per monitoring session -**Technology**: C# (.NET) + SignalR + MQTTnet +**Technology**: C# (.NET) + SignalR + Beckhoff.TwinCAT.Ads + TcpRouter **Authentication**: Backend generates a short-lived token on monitor start; the container validates this token on SignalR connection — no need to implement full SSO in the container. @@ -171,7 +171,8 @@ Build scheduling uses PostgreSQL as the queue backend with MQTT for lightweight **MQTT topics:** - `flowforge/build/notify/{twincat-version}` — backend → build servers (lightweight wake-up signal) - `flowforge/build/progress/{build-id}` — build server → backend (real-time progress) -- `flowforge/deploy/request/{target-id}` — build server → PLC (ADS over MQTT activation, requires TwinCAT Engineering) +- `flowforge/deploy/request/{target-id}` — backend → build server (deploy request) +- `flowforge/deploy/status/{deploy-id}` — build server → backend (deploy progress) ## Authentication & Authorization @@ -242,7 +243,7 @@ project-repo/ 3. **Deploy Phase** (executed by build server): - Backend routes deploy request to the appropriate build server - - Build server activates the TwinCAT solution on the target PLC via ADS over MQTT (requires TwinCAT Engineering) + - Build server activates the TwinCAT solution on the target PLC via direct ADS (requires TwinCAT Engineering) - Separate permissions, blocked by deploy lock on running PLCs - Status updates sent back through SignalR - User receives success/error feedback @@ -251,7 +252,7 @@ project-repo/ - User requests monitoring for a target PLC - Backend starts a dedicated monitor container, returns SignalR endpoint + short-lived token - Frontend connects directly to the monitor container via SignalR - - Monitor container reads PLC data via ADS over MQTT and streams to the frontend + - Monitor container reads PLC data via direct ADS-over-TCP and streams to the frontend - Backend is not in the data path — only manages container lifecycle - Session ends → backend stops the container @@ -361,7 +362,7 @@ All services except the build server run as Docker containers in a single Docker | **Backend** | ASP.NET Core | API server, build orchestration, monitor container lifecycle, admin API (Keycloak facade) | | **Keycloak** | quay.io/keycloak/keycloak | Authentication & authorization — local users, LDAP federation, external SSO (OIDC/SAML). Admin console not exposed; managed via FlowForge admin UI | | **PostgreSQL** | postgres | Metadata, build queue, target registry, audit + Keycloak data | -| **MQTT Broker** | mosquitto / emqx | ADS over MQTT communication, build notifications | +| **MQTT Broker** | mosquitto / emqx | FlowForge internal messaging: build notifications, progress, deploy status | | **docker-socket-proxy** | tecnativa/docker-socket-proxy | Filtered Docker API access for backend (containers only) | | **Monitor Containers** | On-demand (C#/.NET) | Live PLC data streaming via SignalR, created/destroyed per session | @@ -388,7 +389,7 @@ This limits the blast radius if the backend is compromised — it can only manag The build server runs on dedicated Windows Server instances (not in the Docker stack): - **Requirement**: TwinCAT Engineering (Windows desktop dependency for Beckhoff Automation Interface) -- **Connectivity**: Connects to the stack via REST (poll backend for build jobs) and MQTT (receive notifications, send progress, deploy via ADS over MQTT) +- **Connectivity**: Connects to the stack via REST (poll backend for build jobs) and MQTT (receive notifications, send progress). Deploy uses direct ADS via native TwinCAT router. - **Scaling**: One instance per supported TwinCAT version - **Cannot be containerized**: TwinCAT Engineering requires Windows desktop environment with COM interop @@ -420,7 +421,7 @@ Monitor containers are registered with Traefik automatically via Docker labels ( - **Service user**: Minimal permissions — repo creation only - **Authorization**: Role-based access control via Keycloak roles/groups - **Deploy protection**: 4-eyes principle for production targets, deploy lock on running PLCs -- **PLC Access**: ADS over MQTT via central broker (no per-server ADS route configuration) +- **PLC Access**: Direct ADS via Beckhoff.TwinCAT.Ads (TcpRouter for Linux containers, native router for Windows build servers). See [ADS_INTEGRATION.md](doc/ADS_INTEGRATION.md). - **Docker socket isolation**: Backend accesses Docker API only through docker-socket-proxy (filtered to container create/start/stop/remove) - **Monitor container auth**: Short-lived tokens issued by backend, validated by container on SignalR connect — no full auth stack in containers - **Code Injection**: Validate all user inputs to prevent malicious code generation diff --git a/doc/BUILD_SERVER_DESIGN.md b/doc/BUILD_SERVER_DESIGN.md index 252ca7a..7d63cd8 100644 --- a/doc/BUILD_SERVER_DESIGN.md +++ b/doc/BUILD_SERVER_DESIGN.md @@ -2,231 +2,48 @@ ## Overview -The FlowForge Build Server converts visual flow programs into TwinCAT PLC projects using the Beckhoff Automation Interface. It runs on dedicated Windows Server instances (one per TwinCAT version) and connects to the main stack via REST + MQTT. - -This document captures the architectural decisions, constraints, and patterns derived from Beckhoff documentation, community projects, and production experience. - -## Critical Constraints - -| Constraint | Reason | Impact | -|-----------|--------|--------| -| **32-bit (x86) execution** | Automation Interface COM is 32-bit only | `x86` in .csproj | -| **STA threading** | COM interop requires Single-Threaded Apartment | `[STAThread]` on entry + dedicated STA thread for pipeline | -| **IOleMessageFilter** | VS DTE rejects COM calls while busy — crashes without it | Mandatory MessageFilter wrapper | -| **One VS instance per build** | No concurrent DTE usage per process | Queue-based sequential processing | -| **TwinCAT Engineering license** | Required for project creation, compilation, activation | Build server = dedicated Windows Server | -| **Visual Studio DTE dependency** | Automation Interface accessed via `TcXaeShell.DTE.15.0` ProgID | VS or TcXaeShell must be installed | - -## Beckhoff Automation Interface - -The Automation Interface is a COM-based API provided by Beckhoff for programmatic control of TwinCAT Engineering (XAE). Key interfaces: - -### Core COM Interfaces - -- **`EnvDTE.DTE`** — Visual Studio Development Tools Environment. Entry point for solution management (open, build, close). -- **`ITcSysManager`** — TwinCAT System Manager. Root interface for navigating and manipulating the TwinCAT project tree. -- **`ITcSmTreeItem`** — Tree item in the TwinCAT project hierarchy. Provides `CreateChild()`, `LookupChild()`, property access. -- **`ITcPlcIECProject`** — PLC IEC project interface. Provides `GenerateBootProject()`, `CheckAllObjects()`, `PlcLogin()`, `PlcStart()`. - -### Key Methods - -| Interface | Method | Purpose | -|-----------|--------|---------| -| `DTE` | `Solution.Open(path)` | Open TwinCAT solution | -| `DTE` | `Solution.SolutionBuild.Build(true)` | Compile the solution | -| `ITcSysManager` | `LookupTreeItem(path)` | Navigate project tree by path | -| `ITcSmTreeItem` | `CreateChild(name, subType, vInfo, templatePath)` | Add POU, GVL, DUT, task | -| `ITcSmTreeItem` | `ImportChild(path, ...)` | Import from PLCopen XML | -| `ITcSysManager` | `ActivateConfiguration()` | Download config to PLC runtime | -| `ITcSysManager` | `StartRestartTwinCAT()` | Start/restart TwinCAT runtime | -| `ITcPlcIECProject` | `GenerateBootProject(true)` | Generate boot project for autostart | - -### TwinCAT Project Tree Structure - -The TwinCAT system manager organizes projects as a tree: - -``` -TIRC (Root) -├── TIPC (PLC) -│ └── PLC Project -│ ├── POUs/ -│ │ ├── MAIN (PRG) -│ │ ├── FB_Timer (FB) -│ │ └── FC_Calculate (FUN) -│ ├── DUTs/ -│ │ └── ST_MachineState (STRUCT) -│ ├── GVLs/ -│ │ └── GVL_Main -│ └── Libraries/ -├── TINC (NC) -├── TIID (I/O) -│ └── Device 1 (EtherCAT Master) -└── TIRT (Realtime) - └── Task 1 - └── Linked PLC Program -``` - -### Tree Item SubTypes - -| Type | SubType Value | Description | -|------|--------------|-------------| -| POU (Program) | 604 | PROGRAM POU | -| POU (Function Block) | 604 | FUNCTION_BLOCK POU | -| POU (Function) | 604 | FUNCTION POU | -| GVL | 615 | Global Variable List | -| DUT (Struct) | 606 | Data Unit Type (Structure) | -| DUT (Enum) | 606 | Data Unit Type (Enumeration) | -| Task | — | Realtime task | +The build server converts visual flow programs into deployable TwinCAT PLC projects. It runs on dedicated Windows Server instances (one per TwinCAT version) and connects to the FlowForge stack via REST + MQTT. ## Pipeline Architecture -The build server processes jobs through a sequential pipeline of discrete steps. Each step has a single responsibility and operates on a shared `BuildContext`. - -### Pipeline Flow - -``` -CloneRepo → ParseFlow → ValidateFlow → CreateProject → GenerateCode - → ConfigureTasks → Compile → GenerateBootProject → CommitResult - → [DeployStep] (conditional, only for Deploy jobs) -``` - -### BuildContext - -Mutable context object passed through all pipeline steps: - -``` -BuildContext -├── Job — BuildJobDto (from backend) -├── WorkspacePath — temp directory for this build -├── FlowDocument — parsed flow JSON -├── Solution — DTE Solution reference -├── SysManager — ITcSysManager reference -├── PlcProject — ITcSmTreeItem for PLC project node -├── GeneratedFiles — list of generated file paths -├── Errors — accumulated error messages -├── Timings — per-step execution times -└── CancellationToken -``` - -### Step Descriptions - -1. **CloneRepoStep** — Git clone or fetch the project repository using LibGit2Sharp with user credentials from the job. -2. **ParseFlowStep** — Deserialize `flow/program.json` from the cloned repo into a `FlowDocument` object. -3. **ValidateFlowStep** — Structural validation: check all connections reference valid ports, detect cycles, verify required parameters. -4. **CreateProjectStep** — Create a new TwinCAT solution using `DTE`, add PLC project from machine type template via `ITcSmTreeItem.CreateChild()`. -5. **GenerateCodeStep** — Translate flow nodes to IEC 61131-3 Structured Text using node translators. Populate POUs, GVLs, DUTs via `ITcSmTreeItem.CreateChild()`. -6. **ConfigureTasksStep** — Create realtime tasks under TIRT, link PLC programs to tasks. -7. **CompileStep** — Call `DTE.Solution.SolutionBuild.Build(true)`, collect errors from ErrorList. -8. **GenerateBootProjectStep** — Call `ITcPlcIECProject.GenerateBootProject(true)` + `CheckAllObjects()` for autostart capability. -9. **CommitResultStep** — Git commit the generated `.tsproj` and ST files, push to the project repo. -10. **DeployStep** (conditional) — Only for Deploy jobs. Call `ActivateConfiguration()` + `StartRestartTwinCAT()` via `ITcSysManager`. Deploy lock and 4-eyes validation happen in the backend before the job reaches the build server. - -## Code Generation Strategy - -### FlowDocument JSON to IEC 61131-3 ST - -The code generator translates the visual flow (nodes + connections) into Structured Text: - -1. **Topological sort** — Order nodes by data dependencies (connections define edges). -2. **Node translation** — Each node type has a dedicated `INodeTranslator` that emits ST code. -3. **Variable declarations** — Collect all node inputs/outputs, generate `VAR`/`VAR_INPUT`/`VAR_OUTPUT` blocks. -4. **Connection wiring** — Generate assignment statements for connections between nodes. -5. **POU generation** — Wrap translated code in `PROGRAM`/`FUNCTION_BLOCK` declarations. - -### Node Translator Pattern - -Each visual node type maps to an `INodeTranslator` implementation: - -``` -INodeTranslator -├── TimerTranslator — TON/TOF/TP timer blocks -├── CounterTranslator — CTU/CTD/CTUD counter blocks -├── ComparisonTranslator — GT/LT/EQ/GE/LE comparisons -├── MathTranslator — ADD/SUB/MUL/DIV operations -├── LogicTranslator — AND/OR/NOT/XOR gates -├── MoveTranslator — MOVE/assignment operations -└── (extensible per node type) -``` - -Each translator receives the node definition and its resolved input connections, and returns: -- Variable declarations (type, initial value) -- Body statements (ST code) -- Any required library references - -## COM Interop Patterns - -### MessageFilter (Mandatory) - -The `IOleMessageFilter` implementation handles COM call rejection by the DTE when it's busy processing. Without this, random `RPC_E_CALL_REJECTED` exceptions crash the build process. - -Key behavior: -- `HandleInComingCall` — Always returns `SERVERCALL_ISHANDLED` (accept all incoming calls). -- `RetryRejectedCall` — Returns 99ms retry delay when the DTE is busy (`SERVERCALL_RETRYLATER`). This causes the COM runtime to automatically retry the call after a short delay. -- `MessagePending` — Returns `PENDINGMSG_WAITDEFPROCESS` (default processing). - -This pattern is proven in both TcUnit-Verifier and the Beckhoff CodeGenerationDemo samples. - -### VisualStudioInstance Facade - -Wraps `EnvDTE.DTE` lifecycle management behind `IVisualStudioInstance`: - -- **Creation** — `Activator.CreateInstance(Type.GetTypeFromProgID("TcXaeShell.DTE.15.0"))` or version-specific ProgID. -- **Version detection** — Parse .sln file to determine required VS/XAE version, select matching ProgID. -- **UI suppression** — `DTE.SuppressUI = true`, `DTE.MainWindow.Visible = false` for headless operation. -- **Build** — `DTE.Solution.SolutionBuild.Build(true)` with error collection from `DTE.ToolWindows.ErrorList`. -- **Cleanup** — `DTE.Quit()` in `finally` block to prevent orphaned devenv.exe processes. - -### AutomationInterface Facade - -Wraps `ITcSysManager` operations behind `IAutomationInterface`: - -- **Tree navigation** — `LookupTreeItem("TIPC^PLC Project^POUs")` with typed path helpers. -- **Child creation** — `CreateChild(name, subType, vInfo, templatePath)` with subtype constants. -- **Configuration activation** — `ActivateConfiguration()` → downloads to target PLC. -- **Runtime control** — `StartRestartTwinCAT()` → starts/restarts the TwinCAT runtime. - -## Template Management - -Machine type templates accelerate project creation by providing pre-configured PLC project structures: - -- **Template format** — `.tpzip` files (TwinCAT Project ZIP archives). -- **Template types** — 3-axis standalone, 3+1 axis standalone, x-division rotary table, etc. -- **Usage** — `ITcSmTreeItem.CreateChild(name, 0, null, templatePath)` to create PLC project from template. -- **Storage** — Templates stored on the build server file system, path resolved by `TemplateManager` from machine type enum. +Build jobs are processed through a sequential pipeline of `IBuildStep` implementations: -## Build vs Deploy Workflow +1. **CloneRepoStep** — Clone/fetch the project repo from GitHub +2. **ParseFlowStep** — Deserialize flow JSON from the repo +3. **ValidateFlowStep** — Validate node graph structure and connections +4. **GenerateCodeStep** — Generate IEC 61131-3 Structured Text from flow nodes +5. **CreateProjectStep** — Create TwinCAT project structure via Automation Interface +6. **ConfigureTasksStep** — Configure PLC tasks and I/O mappings +7. **CompileStep** — Compile the TwinCAT project +8. **GenerateBootProjectStep** — Generate boot project for target deployment +9. **CommitResultStep** — Commit/push generated PLC solution to the repo +10. **DeployStep** — (Deploy jobs only) Activate on target PLC via ADS -### Build (Build-only) +## Code Generation -1. Backend inserts build job into PostgreSQL queue. -2. Build server claims job via REST (`FOR UPDATE SKIP LOCKED`). -3. Pipeline executes steps 1-9 (CloneRepo through CommitResult). -4. Generated PLC solution is committed and pushed to the project repo. -5. Build result reported to backend via REST. +The `INodeTranslator` strategy pattern maps flow node types to Structured Text: +- Each node type has a dedicated translator (e.g., `TimerTranslator`, `CounterTranslator`, `ComparisonTranslator`) +- `StructuredTextGenerator` orchestrates translation using registered translators +- `PlcProjectBuilder` assembles POUs, GVLs, and DUTs into the TwinCAT project structure -### Deploy (Build + Deploy) +## TwinCAT Integration -1. Backend validates deploy permissions (4-eyes for production, deploy lock check). -2. Backend inserts deploy job into PostgreSQL queue. -3. Build server claims job, executes steps 1-9 (same as build). -4. **DeployStep** (step 10) activates configuration on target PLC: - - `ITcSysManager.ActivateConfiguration()` — downloads project to PLC. - - `ITcSysManager.StartRestartTwinCAT()` — starts/restarts the runtime. - - Both operations go through ADS over MQTT to the target PLC. -5. Deploy result reported to backend via REST. +- **IVisualStudioInstance** — Manages the TwinCAT XAE Shell (Visual Studio) COM automation +- **IAutomationInterface** — Facade over `ITcSysManager` for project creation, POU management, compilation, and activation +- **MessageFilter** — COM message filter for handling OLE callbacks during automation -## ADS over MQTT Deployment +## ADS Integration -PLC activation uses ADS (Automation Device Specification) protocol tunneled over MQTT: +Deploy operations use `Beckhoff.TwinCAT.Ads` for direct ADS communication with target PLCs: -- **Central MQTT broker** — All PLCs and build servers connect to a single broker. -- **No per-server ADS routes** — Eliminates manual route configuration on each PLC. -- **Topic structure** — `flowforge/deploy/request/{target-id}` for deploy commands. -- **Build server as gateway** — The build server has TwinCAT Engineering, which provides the ADS client. It publishes ADS commands to the MQTT broker, which routes them to the target PLC. +- **IAdsDeployClient** — Connect, read state, switch to config mode, restart TwinCAT +- Uses the native TwinCAT router on Windows (no TcpRouter needed) +- Deploy sequence documented in [ADS_INTEGRATION.md](ADS_INTEGRATION.md) -## References +## Build Queue Integration -- [Beckhoff InfoSys — Automation Interface](https://infosys.beckhoff.com/content/1033/tc3_automationinterface/index.html) -- [TcUnit — TwinCAT Unit Testing Framework](https://github.com/tcunit/TcUnit) (VisualStudioInstance pattern, MessageFilter implementation) -- [CodeGenerationDemo](src/build-server/samples/) — Beckhoff sample for Automation Interface scripting -- [AllTwinCAT — CI/CD for TwinCAT](https://alltwincat.com/) — Blog series on Jenkins integration and automated builds +- Build servers poll the backend REST API for jobs (`FOR UPDATE SKIP LOCKED`) +- MQTT `flowforge/build/notify/{twincat-version}` provides lightweight wake-up signals +- Progress reported via MQTT `flowforge/build/progress/{build-id}` +- Deploy status reported via MQTT `flowforge/deploy/status/{deploy-id}` +- Final results reported via backend REST API diff --git a/doc/MODULE_ARCHITECTURE.md b/doc/MODULE_ARCHITECTURE.md index 684bf49..ecb4f1a 100644 --- a/doc/MODULE_ARCHITECTURE.md +++ b/doc/MODULE_ARCHITECTURE.md @@ -1,180 +1,78 @@ # Module Architecture -## Overview +## Shared Library (`FlowForge.Shared`) -FlowForge is composed of four main modules plus a shared library, each with an architecture pattern suited to its complexity and deployment model. +Dependency-free library containing DTOs, enums, and constants shared across all .NET components. -``` -┌─────────────────────────────────────────────────────────┐ -│ FlowForge.Shared │ -│ (DTOs, Enums, Constants) │ -└────────┬──────────────┬──────────────┬──────────────┬────┘ - │ │ │ │ - ┌────▼────┐ ┌─────▼─────┐ ┌────▼────┐ ┌────▼────┐ - │ Backend │ │Build Server│ │ Monitor │ │Frontend │ - │ (3 proj)│ │ (1 proj) │ │ (1 proj)│ │ (React) │ - └─────────┘ └───────────┘ └─────────┘ └─────────┘ -``` +### Models +- **Ads/** — `PlcAdsState`, `PlcStateDto`, `AdsConnectionInfo`, `AdsVariableSubscription` +- **Auth/** — `UserInfoDto` +- **Build/** — `BuildStatus`, `BuildJobDto`, `BuildProgressDto`, `BuildResultDto` +- **Deploy/** — `DeployStatus`, `DeployRequestDto`, `DeployResultDto` +- **Flow/** — `FlowDocument`, `FlowNode`, `FlowConnection`, `FlowPort`, `NodePosition` +- **Monitor/** — `MonitorSessionDto`, `PlcVariableValueDto` +- **Project/** — `ProjectSummaryDto`, `ProjectDetailDto` +- **Target/** — `PlcTargetDto`, `TargetGroupDto` -## Shared DTO Library (`FlowForge.Shared`) +### MQTT +- `MqttTopics` — Topic string builders for FlowForge internal messaging (build notifications, progress, deploy status). ADS communication uses `Beckhoff.TwinCAT.Ads` directly. -**Pattern**: Standalone class library with no external dependencies. +--- -**Rationale**: Multiple .NET modules (Backend, Build Server, Monitor Server) exchange data via REST and MQTT. A shared library ensures type consistency across module boundaries without coupling modules to each other. +## Backend (`FlowForge.Backend`) -**Contents**: -- **Models/** — DTOs for Flow, Build, Deploy, Project, Target, Auth, Monitor domains. -- **Enums/** — Permission, ProjectRole, BuildStatus, DeployStatus. -- **Mqtt/** — Static topic string builders (type-safe topic construction). +Clean Architecture Lite with three layers: -**Design rules**: -- No logic — only data structures and constants. -- No external NuGet dependencies — keeps the library lightweight and universally referenceable. -- Immutable where possible — DTOs use `init` properties. +### Api (ASP.NET Core) +Controllers, middleware, auth (Keycloak JWT validation), SignalR hubs for build/deploy status. -## Backend — Clean Architecture Lite (3 Projects) +### Application (Business Logic) +- **Services**: `ProjectService`, `BuildService`, `DeployService`, `TargetService`, `MonitorService`, `AdminService` +- **Interfaces**: Repository and service contracts (Git, Docker, MQTT, Keycloak, encryption) +- No external dependencies — references Shared only -**Pattern**: Simplified Clean Architecture with three projects: Application (core), Infrastructure (externals), Api (HTTP surface). +### Infrastructure +EF Core (PostgreSQL), external integrations (Git via LibGit2Sharp, MQTT via MQTTnet, Docker API, Keycloak Admin REST API, AES encryption). -**Rationale**: The backend has the highest complexity — it integrates with PostgreSQL, Keycloak, GitHub, MQTT, Docker, and SignalR. Separating concerns into three projects enables: -- Independent testing of business logic (Application) without database or HTTP. -- Swappable infrastructure (e.g., different git providers, different databases). -- Clear dependency direction: Api → Application ← Infrastructure. +--- -### Project: `FlowForge.Backend.Application` +## Build Server (`FlowForge.BuildServer`) -The core business logic layer. References only `FlowForge.Shared`. +Windows-only service for PLC project generation and deployment. -- **Entities/** — Domain entities mapped to PostgreSQL tables (Project, BuildJob, PlcTarget, etc.). -- **Interfaces/** — Repository and service abstractions (IProjectRepository, IGitService, etc.). -- **Services/** — Business logic implementations (ProjectService, BuildService, DeployService, etc.). -- **Validation/** — Domain validation rules (FlowDocumentValidator). +### Pipeline +Sequential `IBuildStep` pipeline: clone → parse → validate → generate code → create project → configure tasks → compile → generate boot project → commit → deploy. -### Project: `FlowForge.Backend.Infrastructure` +### Code Generation +`INodeTranslator` strategy pattern for flow-to-Structured Text translation. `PlcProjectBuilder` for TwinCAT project assembly. -External integrations. References `Application` + `Shared`. +### TwinCAT Integration +- **IAutomationInterface** — COM facade over `ITcSysManager` (Visual Studio/TwinCAT XAE Shell) +- **IAdsDeployClient** — Direct ADS via `Beckhoff.TwinCAT.Ads` for deploy operations (state management, config mode switch, restart). Uses native TwinCAT router. -- **Persistence/** — EF Core DbContext, entity configurations, migrations. -- **Repositories/** — `IRepository` implementations using EF Core. -- **Git/** — LibGit2Sharp implementation of `IGitService`. -- **Mqtt/** — MQTTnet implementation of `IMqttService`. -- **Docker/** — HttpClient-based `IDockerService` (talks to docker-socket-proxy). -- **Keycloak/** — HttpClient-based `IKeycloakAdminService` (Keycloak Admin REST API). -- **Security/** — AES encryption for per-user token storage. -- **DependencyInjection.cs** — `AddInfrastructure()` extension method for clean DI registration. +### Services +- `BuildJobClient` — REST client for backend API (claim jobs, report results) +- `MqttHandler` — FlowForge internal messaging (build notifications, progress updates) +- `Worker` — Background service polling for build jobs -### Project: `FlowForge.Backend.Api` +--- -HTTP surface layer. References `Application` + `Infrastructure` + `Shared`. +## Monitor Server (`FlowForge.MonitorServer`) -- **Controllers/** — REST API endpoints (Projects, Build, Deploy, Targets, Monitor, Admin). -- **Hubs/** — SignalR hubs (BuildHub with typed client interface). -- **Middleware/** — Error handling (ProblemDetails), request logging. -- **Auth/** — Keycloak JWT setup, claims extensions. -- **Configuration/** — Options classes (FlowForgeOptions, KeycloakOptions, MqttOptions). +On-demand Linux/Docker container for live PLC data streaming. -### Dependency Graph +### ADS Client +- **IAdsClient** — Direct ADS via `Beckhoff.TwinCAT.Ads` + `Beckhoff.TwinCAT.Ads.TcpRouter` (software ADS router for non-TwinCAT systems) +- Supports: single reads, batch reads (Sum Commands), ADS notifications (`OnChange`), PLC state reads +- Each container maintains a single long-lived ADS-over-TCP connection to the target PLC -``` -Api ──────→ Application ←────── Infrastructure - │ │ │ - └────────────────┼────────────────────┘ - ▼ - Shared -``` - -**Key rule**: Application has NO reference to Infrastructure. Service interfaces are defined in Application, implemented in Infrastructure, and wired via DI in Api. - -## Build Server — Single Project, Organized Folders - -**Pattern**: Single project with folder-based organization (Pipeline, CodeGen, TwinCat, Services). - -**Rationale**: The build server has a focused responsibility (convert flow → TwinCAT project) and runs as an isolated process on Windows. Multi-project separation would add complexity without proportional benefit. The pipeline pattern provides internal structure. - -See [BUILD_SERVER_DESIGN.md](BUILD_SERVER_DESIGN.md) for detailed design. - -**Folder structure**: -- **Pipeline/** — `IBuildStep` interface, `BuildContext`, `BuildPipeline` orchestrator, concrete steps. -- **CodeGen/** — `ICodeGenerator`, `StructuredTextGenerator`, `PlcProjectBuilder`, node translators. -- **TwinCat/** — COM facades (`IVisualStudioInstance`, `IAutomationInterface`, `MessageFilter`, tree item constants, template manager). -- **Services/** — `BuildJobClient` (existing), `MqttHandler`, `GitWorkspace`, `WorkspaceManager`. - -**Key architectural decisions**: -1. **Pipeline pattern** — Each build step is an `IBuildStep` with `ExecuteAsync(BuildContext, CancellationToken)`. Steps are executed sequentially by `BuildPipeline`. Enables logging, timing, and error handling per step. -2. **Facade pattern for COM** — `IVisualStudioInstance` and `IAutomationInterface` wrap COM objects behind testable interfaces. -3. **Strategy pattern for code generation** — `INodeTranslator` per visual node type. New node types = new translator class. -4. **Template-based project creation** — Machine type templates stored as `.tpzip` files, resolved by `TemplateManager`. - -## Monitor Server — Single Project, Organized Folders - -**Pattern**: Single project with folder-based organization (Hubs, Auth, Services). - -**Rationale**: The monitor server is lightweight — it streams PLC data from MQTT to SignalR. Minimal complexity doesn't warrant multi-project separation. - -**Folder structure**: -- **Hubs/** — `PlcDataHub` (existing) + `IPlcDataHubClient` typed interface. -- **Auth/** — `TokenValidator` for short-lived HMAC tokens. -- **Services/** — `IMqttAdsClient` / `MqttAdsClient` for ADS over MQTT, `SubscriptionManager` for per-connection tracking. +### SignalR Hub +- **PlcDataHub** — Frontend subscribes to PLC variables; hub streams values via `IPlcDataHubClient` +- **IPlcDataHubClient** — `ReceiveVariableValues`, `ReceiveConnectionStatus`, `ReceiveError` -## Frontend — Feature-Based Folders +### Services +- **SubscriptionManager** — Thread-safe tracking of per-connection variable subscriptions +- **TokenValidator** — Short-lived auth token validation for SignalR connections -**Pattern**: Feature-based folder organization with shared utilities. - -**Rationale**: Feature-based structure scales better than layer-based (e.g., all components in one folder). Each feature is self-contained with its own components, hooks, and types. Shared code lives in `shared/`. - -**Folder structure**: -``` -src/ -├── api/ — HTTP client, endpoint constants, TypeScript types -├── auth/ — Keycloak OIDC provider, auth hooks, route guards -├── layout/ — App shell (sidebar, header, content area) -├── features/ -│ ├── editor/ — Flow canvas, node palette, inspector, custom nodes -│ ├── projects/ — Project list, create, templates -│ ├── build/ — Build panel, history, logs -│ ├── deploy/ — Deploy panel, lock indicator, approval dialog -│ ├── targets/ — PLC target management -│ ├── monitoring/ — Live PLC data monitoring -│ └── admin/ — User management (Keycloak facade) -└── shared/ — Reusable components, hooks, utilities -``` - -**Key dependencies**: -- `zustand` — Lightweight state management (simpler than Redux for this scale). -- `@tanstack/react-query` — Server state management (caching, refetching, optimistic updates). -- `keycloak-js` — Keycloak OIDC client adapter. -- `@microsoft/signalr` — SignalR client for real-time updates. -- `react-router-dom` — Client-side routing. - -## Test Project Organization - -**Pattern**: One test project per source project, using xUnit + NSubstitute + FluentAssertions. - -``` -test/ -├── FlowForge.Shared.Tests/ -├── FlowForge.Backend.Api.Tests/ -├── FlowForge.Backend.Application.Tests/ -├── FlowForge.Backend.Infrastructure.Tests/ (+Testcontainers.PostgreSql) -├── FlowForge.BuildServer.Tests/ -└── FlowForge.MonitorServer.Tests/ -``` - -**Rationale**: -- **xUnit** — Modern, extensible, widely used in .NET ecosystem. -- **NSubstitute** — Clean mocking syntax, ideal for interface-heavy architecture. -- **FluentAssertions** — Readable assertion syntax, better error messages. -- **Testcontainers** — Real PostgreSQL for Infrastructure tests (no in-memory fakes). - -## Technology Choices Summary - -| Module | Runtime | Key Libraries | -|--------|---------|---------------| -| Shared | .NET 9.0 | (none) | -| Backend.Application | .NET 9.0 | (none — interfaces only) | -| Backend.Infrastructure | .NET 9.0 | EF Core, Npgsql, MQTTnet, LibGit2Sharp | -| Backend.Api | .NET 9.0 (ASP.NET Core) | SignalR, JWT Bearer | -| Build Server | .NET 9.0 (x86) | MQTTnet, LibGit2Sharp, COM Interop | -| Monitor Server | .NET 9.0 (ASP.NET Core) | SignalR, MQTTnet | -| Frontend | TypeScript/React 19 | React Flow, zustand, react-query, keycloak-js, signalr | -| Tests | .NET 9.0 | xUnit, NSubstitute, FluentAssertions, Testcontainers | +### Lifecycle +Backend creates container on monitoring request, injects config as env vars (`MonitorOptions`), Traefik auto-discovers via Docker labels, frontend connects directly via SignalR, backend destroys on session end. diff --git a/src/backend/src/FlowForge.Backend.Application/Services/DeployService.cs b/src/backend/src/FlowForge.Backend.Application/Services/DeployService.cs index 9aeb657..39c2ad7 100644 --- a/src/backend/src/FlowForge.Backend.Application/Services/DeployService.cs +++ b/src/backend/src/FlowForge.Backend.Application/Services/DeployService.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later using FlowForge.Backend.Application.Interfaces.Repositories; +using FlowForge.Backend.Application.Interfaces.Services; +using FlowForge.Shared.Models.Deploy; namespace FlowForge.Backend.Application.Services; @@ -9,10 +11,19 @@ public class DeployService { private readonly IDeployRecordRepository _deployRecordRepository; private readonly IPlcTargetRepository _targetRepository; + private readonly IMqttService _mqttService; - public DeployService(IDeployRecordRepository deployRecordRepository, IPlcTargetRepository targetRepository) + public DeployService( + IDeployRecordRepository deployRecordRepository, + IPlcTargetRepository targetRepository, + IMqttService mqttService) { _deployRecordRepository = deployRecordRepository; _targetRepository = targetRepository; + _mqttService = mqttService; } + + // TODO: RequestDeployAsync(DeployRequestDto) — validate target, check deploy lock, create record, queue job + // TODO: ApproveDeployAsync(Guid deployId, string approverId) — 4-eyes approval for production targets + // TODO: GetDeployStatusAsync(Guid deployId) — return current deploy status } diff --git a/src/backend/src/FlowForge.Backend.Application/Services/TargetService.cs b/src/backend/src/FlowForge.Backend.Application/Services/TargetService.cs index dfd668d..0975b18 100644 --- a/src/backend/src/FlowForge.Backend.Application/Services/TargetService.cs +++ b/src/backend/src/FlowForge.Backend.Application/Services/TargetService.cs @@ -13,4 +13,8 @@ public TargetService(IPlcTargetRepository targetRepository) { _targetRepository = targetRepository; } + + // TODO: GetTargetsAsync() — list all targets with cached PLC state (PlcAdsState from last known read) + // TODO: SetDeployLockAsync(Guid targetId, bool locked) — manual deploy lock toggle + // TODO: IsDeployAllowedAsync(Guid targetId, string userId) — checks lock + production rules + 4-eyes } diff --git a/src/build-server/src/FlowForge.BuildServer/FlowForge.BuildServer.csproj b/src/build-server/src/FlowForge.BuildServer/FlowForge.BuildServer.csproj index 79c4d4c..a551558 100644 --- a/src/build-server/src/FlowForge.BuildServer/FlowForge.BuildServer.csproj +++ b/src/build-server/src/FlowForge.BuildServer/FlowForge.BuildServer.csproj @@ -11,6 +11,7 @@ + diff --git a/src/build-server/src/FlowForge.BuildServer/Pipeline/BuildContext.cs b/src/build-server/src/FlowForge.BuildServer/Pipeline/BuildContext.cs index bfc5503..0a825d5 100644 --- a/src/build-server/src/FlowForge.BuildServer/Pipeline/BuildContext.cs +++ b/src/build-server/src/FlowForge.BuildServer/Pipeline/BuildContext.cs @@ -1,6 +1,7 @@ // Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) // SPDX-License-Identifier: AGPL-3.0-or-later +using FlowForge.Shared.Models.Ads; using FlowForge.Shared.Models.Build; using FlowForge.Shared.Models.Flow; @@ -11,6 +12,7 @@ public class BuildContext public required BuildJobDto Job { get; init; } public string WorkspacePath { get; set; } = string.Empty; public FlowDocument? FlowDocument { get; set; } + public AdsConnectionInfo? TargetConnectionInfo { get; set; } public List GeneratedFiles { get; } = []; public List Errors { get; } = []; public Dictionary Timings { get; } = []; diff --git a/src/build-server/src/FlowForge.BuildServer/Pipeline/Steps/DeployStep.cs b/src/build-server/src/FlowForge.BuildServer/Pipeline/Steps/DeployStep.cs index bde77d3..fcfa13e 100644 --- a/src/build-server/src/FlowForge.BuildServer/Pipeline/Steps/DeployStep.cs +++ b/src/build-server/src/FlowForge.BuildServer/Pipeline/Steps/DeployStep.cs @@ -1,15 +1,41 @@ // Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) // SPDX-License-Identifier: AGPL-3.0-or-later +using FlowForge.BuildServer.TwinCat; + namespace FlowForge.BuildServer.Pipeline.Steps; public class DeployStep : IBuildStep { + private readonly IAdsDeployClient _adsClient; + private readonly IAutomationInterface _automationInterface; + private readonly ILogger _logger; + + public DeployStep(IAdsDeployClient adsClient, IAutomationInterface automationInterface, ILogger logger) + { + _adsClient = adsClient; + _automationInterface = automationInterface; + _logger = logger; + } + public string Name => "Deploy"; public Task ExecuteAsync(BuildContext context, CancellationToken ct) { - // TODO: ActivateConfiguration() + StartRestartTwinCAT() via ITcSysManager (conditional — deploy jobs only) + if (!context.Job.IncludeDeploy || context.TargetConnectionInfo is null) + { + _logger.LogInformation("Deploy skipped — build-only job"); + return Task.CompletedTask; + } + + // TODO: Implement deploy sequence (see doc/ADS_INTEGRATION.md): + // 1. Connect to target PLC via ADS (_adsClient.ConnectAsync) + // 2. Read PLC state — deploy lock check (_adsClient.ReadPlcStateAsync) + // 3. Switch to config mode (_adsClient.SwitchToConfigModeAsync) + // 4. Activate configuration via Automation Interface (_automationInterface.ActivateConfiguration) + // 5. Start/restart TwinCAT via ADS (_adsClient.StartRestartTwinCatAsync) + // 6. Verify PLC is in Run state (_adsClient.ReadPlcStateAsync) + // 7. Disconnect (_adsClient.DisconnectAsync) throw new NotImplementedException(); } } diff --git a/src/build-server/src/FlowForge.BuildServer/Program.cs b/src/build-server/src/FlowForge.BuildServer/Program.cs index 0777494..da5c103 100644 --- a/src/build-server/src/FlowForge.BuildServer/Program.cs +++ b/src/build-server/src/FlowForge.BuildServer/Program.cs @@ -4,6 +4,7 @@ using FlowForge.BuildServer; using FlowForge.BuildServer.Configuration; using FlowForge.BuildServer.Services; +using FlowForge.BuildServer.TwinCat; var builder = WebApplication.CreateBuilder(args); @@ -22,6 +23,11 @@ client.BaseAddress = new Uri(backendUrl); }); +// --------------------------------------------------------------------------- +// ADS deploy client (direct ADS via native TwinCAT router — see doc/ADS_INTEGRATION.md) +// --------------------------------------------------------------------------- +builder.Services.AddTransient(); + // --------------------------------------------------------------------------- // Background worker (build job polling) // --------------------------------------------------------------------------- diff --git a/src/build-server/src/FlowForge.BuildServer/Services/MqttHandler.cs b/src/build-server/src/FlowForge.BuildServer/Services/MqttHandler.cs index 9ad2b7c..c507265 100644 --- a/src/build-server/src/FlowForge.BuildServer/Services/MqttHandler.cs +++ b/src/build-server/src/FlowForge.BuildServer/Services/MqttHandler.cs @@ -3,8 +3,11 @@ namespace FlowForge.BuildServer.Services; +/// +/// MQTT handler for FlowForge internal messaging only. +/// ADS communication uses Beckhoff.TwinCAT.Ads directly — see doc/ADS_INTEGRATION.md. +/// public class MqttHandler { - // TODO: MQTT subscribe for build notifications, publish progress updates, - // relay ADS commands for deployment + // TODO: MQTT subscribe for build notifications, publish progress/deploy status updates } diff --git a/src/build-server/src/FlowForge.BuildServer/TwinCat/AdsDeployClient.cs b/src/build-server/src/FlowForge.BuildServer/TwinCat/AdsDeployClient.cs new file mode 100644 index 0000000..16a4039 --- /dev/null +++ b/src/build-server/src/FlowForge.BuildServer/TwinCat/AdsDeployClient.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +using FlowForge.Shared.Models.Ads; + +namespace FlowForge.BuildServer.TwinCat; + +/// +/// Wraps Beckhoff.TwinCAT.Ads.AdsClient for deploy operations. +/// Uses the native TwinCAT router (no TcpRouter needed on Windows). +/// +public class AdsDeployClient : IAdsDeployClient +{ + private readonly ILogger _logger; + + public AdsDeployClient(ILogger logger) + { + _logger = logger; + } + + public Task ConnectAsync(AdsConnectionInfo connectionInfo, CancellationToken ct) + { + // TODO: Connect via AmsNetId.Parse(connectionInfo.AmsNetId), connectionInfo.AdsPort + throw new NotImplementedException(); + } + + public Task ReadPlcStateAsync(CancellationToken ct) + { + // TODO: client.ReadState() → map AdsState to PlcAdsState + throw new NotImplementedException(); + } + + public Task SwitchToConfigModeAsync(CancellationToken ct) + { + // TODO: client.WriteControl(new StateInfo(AdsState.Reconfig, 0)) + throw new NotImplementedException(); + } + + public Task StartRestartTwinCatAsync(CancellationToken ct) + { + // TODO: client.WriteControl(new StateInfo(AdsState.Run, 0)) + throw new NotImplementedException(); + } + + public Task ReadTwinCatVersionAsync(CancellationToken ct) + { + // TODO: Read TwinCAT version from target via ADS device info + throw new NotImplementedException(); + } + + public Task DisconnectAsync(CancellationToken ct) + { + // TODO: Disconnect AdsClient + throw new NotImplementedException(); + } + + public ValueTask DisposeAsync() + { + _logger.LogInformation("Disposing ADS deploy client"); + return ValueTask.CompletedTask; + } +} diff --git a/src/build-server/src/FlowForge.BuildServer/TwinCat/IAdsDeployClient.cs b/src/build-server/src/FlowForge.BuildServer/TwinCat/IAdsDeployClient.cs new file mode 100644 index 0000000..edbc502 --- /dev/null +++ b/src/build-server/src/FlowForge.BuildServer/TwinCat/IAdsDeployClient.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +using FlowForge.Shared.Models.Ads; + +namespace FlowForge.BuildServer.TwinCat; + +/// +/// ADS client for deploy-time operations on target PLCs. +/// Uses the native TwinCAT router on the Windows build server. +/// +public interface IAdsDeployClient : IAsyncDisposable +{ + Task ConnectAsync(AdsConnectionInfo connectionInfo, CancellationToken ct = default); + Task ReadPlcStateAsync(CancellationToken ct = default); + Task SwitchToConfigModeAsync(CancellationToken ct = default); + Task StartRestartTwinCatAsync(CancellationToken ct = default); + Task ReadTwinCatVersionAsync(CancellationToken ct = default); + Task DisconnectAsync(CancellationToken ct = default); +} diff --git a/src/monitor-server/src/FlowForge.MonitorServer/Configuration/MonitorOptions.cs b/src/monitor-server/src/FlowForge.MonitorServer/Configuration/MonitorOptions.cs index d725e68..b949c34 100644 --- a/src/monitor-server/src/FlowForge.MonitorServer/Configuration/MonitorOptions.cs +++ b/src/monitor-server/src/FlowForge.MonitorServer/Configuration/MonitorOptions.cs @@ -17,7 +17,7 @@ public class MonitorOptions /// Short-lived auth token for frontend SignalR connections. public string AuthToken { get; set; } = string.Empty; - /// MQTT broker host for ADS over MQTT. + /// MQTT broker host for FlowForge internal messaging. public string MqttHost { get; set; } = "mqtt-broker"; /// MQTT broker port. @@ -25,4 +25,13 @@ public class MonitorOptions /// Target PLC AMS Net ID to monitor. public string TargetAmsNetId { get; set; } = string.Empty; -} \ No newline at end of file + + /// Hostname or IP of the target PLC for TcpRouter connection. + public string? TargetHostname { get; set; } + + /// ADS port on the target PLC (default 851 = PLC Runtime 1). + public int AdsPort { get; set; } = 851; + + /// TCP port for the ADS router on the target (default 48898). + public int AdsTcpPort { get; set; } = 48898; +} diff --git a/src/monitor-server/src/FlowForge.MonitorServer/FlowForge.MonitorServer.csproj b/src/monitor-server/src/FlowForge.MonitorServer/FlowForge.MonitorServer.csproj index 2470c34..d172a82 100644 --- a/src/monitor-server/src/FlowForge.MonitorServer/FlowForge.MonitorServer.csproj +++ b/src/monitor-server/src/FlowForge.MonitorServer/FlowForge.MonitorServer.csproj @@ -8,6 +8,8 @@ + + diff --git a/src/monitor-server/src/FlowForge.MonitorServer/Hubs/PlcDataHub.cs b/src/monitor-server/src/FlowForge.MonitorServer/Hubs/PlcDataHub.cs index 9e20db6..3da769c 100644 --- a/src/monitor-server/src/FlowForge.MonitorServer/Hubs/PlcDataHub.cs +++ b/src/monitor-server/src/FlowForge.MonitorServer/Hubs/PlcDataHub.cs @@ -1,6 +1,8 @@ // Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) // SPDX-License-Identifier: AGPL-3.0-or-later +using FlowForge.MonitorServer.Services; +using FlowForge.Shared.Models.Ads; using Microsoft.AspNetCore.SignalR; namespace FlowForge.MonitorServer.Hubs; @@ -11,10 +13,14 @@ namespace FlowForge.MonitorServer.Hubs; /// public class PlcDataHub : Hub { + private readonly IAdsClient _adsClient; + private readonly SubscriptionManager _subscriptionManager; private readonly ILogger _logger; - public PlcDataHub(ILogger logger) + public PlcDataHub(IAdsClient adsClient, SubscriptionManager subscriptionManager, ILogger logger) { + _adsClient = adsClient; + _subscriptionManager = subscriptionManager; _logger = logger; } @@ -27,18 +33,41 @@ public override Task OnConnectedAsync() public override Task OnDisconnectedAsync(Exception? exception) { + _subscriptionManager.RemoveAllSubscriptions(Context.ConnectionId); _logger.LogInformation("Client disconnected: {ConnectionId}", Context.ConnectionId); return base.OnDisconnectedAsync(exception); } /// - /// Client subscribes to specific PLC variables. + /// Client subscribes to specific PLC variables via direct ADS notifications. /// public async Task Subscribe(string[] variablePaths) { - // TODO: Register ADS subscriptions via MQTT for requested variables + foreach (var path in variablePaths) + { + _subscriptionManager.AddSubscription(Context.ConnectionId, path); + + var subscription = new AdsVariableSubscription { VariablePath = path }; + // TODO: Wire callback to push values to this client via IPlcDataHubClient.ReceiveVariableValues + await _adsClient.SubscribeAsync(subscription, _ => { }); + } + _logger.LogInformation("Client {ConnectionId} subscribed to {Count} variables", Context.ConnectionId, variablePaths.Length); - await Task.CompletedTask; } -} \ No newline at end of file + + /// + /// Client unsubscribes from specific PLC variables. + /// + public async Task Unsubscribe(string[] variablePaths) + { + foreach (var path in variablePaths) + { + _subscriptionManager.RemoveSubscription(Context.ConnectionId, path); + await _adsClient.UnsubscribeAsync(path); + } + + _logger.LogInformation("Client {ConnectionId} unsubscribed from {Count} variables", + Context.ConnectionId, variablePaths.Length); + } +} diff --git a/src/monitor-server/src/FlowForge.MonitorServer/Program.cs b/src/monitor-server/src/FlowForge.MonitorServer/Program.cs index e009025..2c48e10 100644 --- a/src/monitor-server/src/FlowForge.MonitorServer/Program.cs +++ b/src/monitor-server/src/FlowForge.MonitorServer/Program.cs @@ -3,6 +3,7 @@ using FlowForge.MonitorServer.Configuration; using FlowForge.MonitorServer.Hubs; +using FlowForge.MonitorServer.Services; var builder = WebApplication.CreateBuilder(args); @@ -12,6 +13,13 @@ builder.Services.Configure( builder.Configuration.GetSection(MonitorOptions.Section)); +// --------------------------------------------------------------------------- +// ADS client (direct ADS-over-TCP via TcpRouter — see doc/ADS_INTEGRATION.md) +// --------------------------------------------------------------------------- +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +// TODO: Add IHostedService to establish ADS connection on startup using MonitorOptions + // --------------------------------------------------------------------------- // SignalR // --------------------------------------------------------------------------- @@ -29,4 +37,4 @@ // --------------------------------------------------------------------------- app.MapHub("/plc"); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/src/monitor-server/src/FlowForge.MonitorServer/Services/AdsClientWrapper.cs b/src/monitor-server/src/FlowForge.MonitorServer/Services/AdsClientWrapper.cs new file mode 100644 index 0000000..559d0e8 --- /dev/null +++ b/src/monitor-server/src/FlowForge.MonitorServer/Services/AdsClientWrapper.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +using FlowForge.Shared.Models.Ads; +using FlowForge.Shared.Models.Monitor; + +namespace FlowForge.MonitorServer.Services; + +/// +/// Wraps Beckhoff.TwinCAT.Ads.AdsClient with TcpRouter for use on Linux/Docker. +/// Each monitor container maintains a single long-lived ADS connection. +/// +public class AdsClientWrapper : IAdsClient +{ + private readonly ILogger _logger; + + public AdsClientWrapper(ILogger logger) + { + _logger = logger; + } + + public Task ConnectAsync(AdsConnectionInfo connectionInfo, CancellationToken ct) + { + // TODO: Start TcpRouter with unique local AmsNetId + // TODO: Connect AdsClient to target via AmsNetId.Parse(connectionInfo.AmsNetId), connectionInfo.AdsPort + throw new NotImplementedException(); + } + + public Task ReadVariableAsync(string variablePath, CancellationToken ct) + { + // TODO: CreateVariableHandle → ReadAny → DeleteVariableHandle + throw new NotImplementedException(); + } + + public Task> ReadVariablesBatchAsync( + IReadOnlyList variablePaths, CancellationToken ct) + { + // TODO: Use ADS Sum Commands for batch reads (max 500 per call) + throw new NotImplementedException(); + } + + public Task SubscribeAsync(AdsVariableSubscription subscription, Action callback, CancellationToken ct) + { + // TODO: AddDeviceNotificationEx with AdsTransMode.OnChange + // Max 1024 notifications per connection + throw new NotImplementedException(); + } + + public Task UnsubscribeAsync(string variablePath, CancellationToken ct) + { + // TODO: DeleteDeviceNotification for the given variable + throw new NotImplementedException(); + } + + public Task ReadPlcStateAsync(CancellationToken ct) + { + // TODO: client.ReadState() → map AdsState to PlcAdsState + throw new NotImplementedException(); + } + + public Task DisconnectAsync(CancellationToken ct) + { + // TODO: Disconnect AdsClient, dispose TcpRouter + throw new NotImplementedException(); + } + + public ValueTask DisposeAsync() + { + // TODO: Clean up AdsClient and TcpRouter resources + _logger.LogInformation("Disposing ADS client wrapper"); + return ValueTask.CompletedTask; + } +} diff --git a/src/monitor-server/src/FlowForge.MonitorServer/Services/IAdsClient.cs b/src/monitor-server/src/FlowForge.MonitorServer/Services/IAdsClient.cs new file mode 100644 index 0000000..7f5ba53 --- /dev/null +++ b/src/monitor-server/src/FlowForge.MonitorServer/Services/IAdsClient.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +using FlowForge.Shared.Models.Ads; +using FlowForge.Shared.Models.Monitor; + +namespace FlowForge.MonitorServer.Services; + +/// +/// ADS client for reading PLC variables and subscribing to value changes. +/// Wraps Beckhoff.TwinCAT.Ads.AdsClient with TcpRouter for Linux/Docker. +/// +public interface IAdsClient : IAsyncDisposable +{ + Task ConnectAsync(AdsConnectionInfo connectionInfo, CancellationToken ct = default); + Task ReadVariableAsync(string variablePath, CancellationToken ct = default); + Task> ReadVariablesBatchAsync( + IReadOnlyList variablePaths, CancellationToken ct = default); + Task SubscribeAsync(AdsVariableSubscription subscription, Action callback, CancellationToken ct = default); + Task UnsubscribeAsync(string variablePath, CancellationToken ct = default); + Task ReadPlcStateAsync(CancellationToken ct = default); + Task DisconnectAsync(CancellationToken ct = default); +} diff --git a/src/monitor-server/src/FlowForge.MonitorServer/Services/IMqttAdsClient.cs b/src/monitor-server/src/FlowForge.MonitorServer/Services/IMqttAdsClient.cs deleted file mode 100644 index fba2c9d..0000000 --- a/src/monitor-server/src/FlowForge.MonitorServer/Services/IMqttAdsClient.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) -// SPDX-License-Identifier: AGPL-3.0-or-later - -using FlowForge.Shared.Models.Monitor; - -namespace FlowForge.MonitorServer.Services; - -public interface IMqttAdsClient -{ - Task ConnectAsync(CancellationToken ct = default); - Task ReadVariableAsync(string variablePath, CancellationToken ct = default); - Task SubscribeAsync(string variablePath, Action callback, CancellationToken ct = default); - Task UnsubscribeAsync(string variablePath, CancellationToken ct = default); - Task DisconnectAsync(CancellationToken ct = default); -} diff --git a/src/monitor-server/src/FlowForge.MonitorServer/Services/MqttAdsClient.cs b/src/monitor-server/src/FlowForge.MonitorServer/Services/MqttAdsClient.cs deleted file mode 100644 index 945c54d..0000000 --- a/src/monitor-server/src/FlowForge.MonitorServer/Services/MqttAdsClient.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) -// SPDX-License-Identifier: AGPL-3.0-or-later - -using FlowForge.Shared.Models.Monitor; - -namespace FlowForge.MonitorServer.Services; - -public class MqttAdsClient : IMqttAdsClient -{ - public Task ConnectAsync(CancellationToken ct) - { - // TODO: Connect to MQTT broker using MQTTnet - throw new NotImplementedException(); - } - - public Task ReadVariableAsync(string variablePath, CancellationToken ct) - { - // TODO: Publish ADS read request via MQTT, await response - throw new NotImplementedException(); - } - - public Task SubscribeAsync(string variablePath, Action callback, CancellationToken ct) - { - // TODO: Register ADS subscription via MQTT - throw new NotImplementedException(); - } - - public Task UnsubscribeAsync(string variablePath, CancellationToken ct) - { - // TODO: Unregister ADS subscription - throw new NotImplementedException(); - } - - public Task DisconnectAsync(CancellationToken ct) - { - // TODO: Disconnect from MQTT broker - throw new NotImplementedException(); - } -} diff --git a/src/shared/FlowForge.Shared/Models/Ads/AdsConnectionInfo.cs b/src/shared/FlowForge.Shared/Models/Ads/AdsConnectionInfo.cs new file mode 100644 index 0000000..ed39d17 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Ads/AdsConnectionInfo.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Ads; + +public record AdsConnectionInfo +{ + public string AmsNetId { get; init; } = string.Empty; + public int AdsPort { get; init; } = 851; + + /// + /// Hostname or IP of the target PLC. Used by TcpRouter on non-TwinCAT + /// systems (e.g. Linux Docker containers) to establish ADS-over-TCP. + /// + public string? TargetHostname { get; init; } + + /// TCP port for the ADS router on the target (default 48898). + public int TcpPort { get; init; } = 48898; +} diff --git a/src/shared/FlowForge.Shared/Models/Ads/AdsVariableSubscription.cs b/src/shared/FlowForge.Shared/Models/Ads/AdsVariableSubscription.cs new file mode 100644 index 0000000..0c161a1 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Ads/AdsVariableSubscription.cs @@ -0,0 +1,11 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Ads; + +public record AdsVariableSubscription +{ + public string VariablePath { get; init; } = string.Empty; + public int CycleTimeMs { get; init; } = 100; + public int MaxDelayMs { get; init; } +} diff --git a/src/shared/FlowForge.Shared/Models/Ads/PlcAdsState.cs b/src/shared/FlowForge.Shared/Models/Ads/PlcAdsState.cs new file mode 100644 index 0000000..a64b8ec --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Ads/PlcAdsState.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Ads; + +/// +/// Mirrors TwinCAT.Ads.AdsState for use in shared DTOs without requiring a +/// Beckhoff NuGet dependency in the Shared library. +/// See: https://infosys.beckhoff.com/content/1033/tc3_adsnetref/7313023115.html +/// +public enum PlcAdsState +{ + Invalid = 0, + Idle = 1, + Reset = 2, + Init = 3, + Start = 4, + Run = 5, + Stop = 6, + SaveConfig = 7, + LoadConfig = 8, + PowerFailure = 9, + PowerGood = 10, + Error = 11, + Shutdown = 12, + Suspend = 13, + Resume = 14, + Config = 15, + Reconfig = 16 +} diff --git a/src/shared/FlowForge.Shared/Models/Ads/PlcStateDto.cs b/src/shared/FlowForge.Shared/Models/Ads/PlcStateDto.cs new file mode 100644 index 0000000..d8bffa4 --- /dev/null +++ b/src/shared/FlowForge.Shared/Models/Ads/PlcStateDto.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace FlowForge.Shared.Models.Ads; + +public record PlcStateDto +{ + public string AmsNetId { get; init; } = string.Empty; + public PlcAdsState State { get; init; } + public DateTimeOffset Timestamp { get; init; } + + public bool IsRunning => State == PlcAdsState.Run; + public bool IsInConfigMode => State is PlcAdsState.Config or PlcAdsState.Reconfig; + public bool IsSafeForDeploy => State is PlcAdsState.Stop or PlcAdsState.Config; +} diff --git a/src/shared/FlowForge.Shared/Models/Deploy/DeployRequestDto.cs b/src/shared/FlowForge.Shared/Models/Deploy/DeployRequestDto.cs index af1ca3d..28a4d44 100644 --- a/src/shared/FlowForge.Shared/Models/Deploy/DeployRequestDto.cs +++ b/src/shared/FlowForge.Shared/Models/Deploy/DeployRequestDto.cs @@ -8,4 +8,5 @@ public record DeployRequestDto public Guid ProjectId { get; init; } public string TargetAmsNetId { get; init; } = string.Empty; public string? ApproverId { get; init; } + public int AdsPort { get; init; } = 851; } diff --git a/src/shared/FlowForge.Shared/Models/Target/PlcTargetDto.cs b/src/shared/FlowForge.Shared/Models/Target/PlcTargetDto.cs index 8bb3d03..ced51d5 100644 --- a/src/shared/FlowForge.Shared/Models/Target/PlcTargetDto.cs +++ b/src/shared/FlowForge.Shared/Models/Target/PlcTargetDto.cs @@ -1,6 +1,8 @@ // Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) // SPDX-License-Identifier: AGPL-3.0-or-later +using FlowForge.Shared.Models.Ads; + namespace FlowForge.Shared.Models.Target; public record PlcTargetDto @@ -13,4 +15,5 @@ public record PlcTargetDto public Guid? GroupId { get; init; } public bool IsProductionTarget { get; init; } public bool DeployLocked { get; init; } + public PlcAdsState? CurrentState { get; init; } } diff --git a/src/shared/FlowForge.Shared/Mqtt/MqttTopics.cs b/src/shared/FlowForge.Shared/Mqtt/MqttTopics.cs index a89f4bc..40f1906 100644 --- a/src/shared/FlowForge.Shared/Mqtt/MqttTopics.cs +++ b/src/shared/FlowForge.Shared/Mqtt/MqttTopics.cs @@ -3,6 +3,10 @@ namespace FlowForge.Shared.Mqtt; +/// +/// MQTT topics for FlowForge internal messaging. +/// ADS communication is handled directly via Beckhoff.TwinCAT.Ads — see doc/ADS_INTEGRATION.md. +/// public static class MqttTopics { private const string Prefix = "flowforge"; @@ -16,12 +20,6 @@ public static string BuildProgress(Guid buildId) => public static string DeployRequest(string targetId) => $"{Prefix}/deploy/request/{targetId}"; - public static string AdsRead(string amsNetId) => - $"{Prefix}/ads/read/{amsNetId}"; - - public static string AdsWrite(string amsNetId) => - $"{Prefix}/ads/write/{amsNetId}"; - - public static string AdsNotification(string amsNetId) => - $"{Prefix}/ads/notification/{amsNetId}"; + public static string DeployStatus(Guid deployId) => + $"{Prefix}/deploy/status/{deployId}"; } diff --git a/test/FlowForge.MonitorServer.Tests/AdsClientWrapperTests.cs b/test/FlowForge.MonitorServer.Tests/AdsClientWrapperTests.cs new file mode 100644 index 0000000..0196ff3 --- /dev/null +++ b/test/FlowForge.MonitorServer.Tests/AdsClientWrapperTests.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +using FlowForge.MonitorServer.Services; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace FlowForge.MonitorServer.Tests; + +public class AdsClientWrapperTests +{ + [Fact] + public async Task DisposeAsync_ShouldNotThrow() + { + var wrapper = new AdsClientWrapper(NullLogger.Instance); + + await wrapper.DisposeAsync(); + } +}