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/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/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(); + } +}