refactor(browse): rename BrowseOpcUaNode* to protocol-agnostic BrowseNode*
Renames BrowseOpcUaNodeCommand/Result -> BrowseNodeCommand/Result and CommunicationService.BrowseOpcUaNodeAsync -> BrowseNodeAsync across Commons, Communication, SiteRuntime, DCL actors, and CentralUI. Wire manifest name follows (BrowseOpcUaNode -> BrowseNode). Browse regression tests green.
This commit is contained in:
@@ -21,7 +21,6 @@ This document serves as the master index for the SCADA system design. The system
|
|||||||
|
|
||||||
### Scale
|
### Scale
|
||||||
|
|
||||||
- ~10 site clusters, each with 50–500 machines, 25–75 live tags per machine.
|
|
||||||
- Central cluster: 2-node active/standby behind a load balancer.
|
- Central cluster: 2-node active/standby behind a load balancer.
|
||||||
- Site clusters: 2-node active/standby, headless (no UI).
|
- Site clusters: 2-node active/standby, headless (no UI).
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"planPath": "docs/plans/2026-05-28-mxgateway-data-connection.md",
|
"planPath": "docs/plans/2026-05-28-mxgateway-data-connection.md",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{"id": 6, "planTask": 1, "subject": "Task 1: Packaging foundation (Gitea feed + package refs)", "status": "pending"},
|
{"id": 6, "planTask": 1, "subject": "Task 1: Packaging foundation (Gitea feed + package refs)", "status": "completed"},
|
||||||
{"id": 7, "planTask": 2, "subject": "Task 2: MxGatewayEndpointConfig type", "status": "pending"},
|
{"id": 7, "planTask": 2, "subject": "Task 2: MxGatewayEndpointConfig type", "status": "completed"},
|
||||||
{"id": 8, "planTask": 3, "subject": "Task 3: MxGatewayEndpointConfigSerializer + tests", "status": "pending", "blockedBy": [7]},
|
{"id": 8, "planTask": 3, "subject": "Task 3: MxGatewayEndpointConfigSerializer + tests", "status": "pending", "blockedBy": [7]},
|
||||||
{"id": 9, "planTask": 4, "subject": "Task 4: MxGatewayEndpointConfigValidator + tests", "status": "pending", "blockedBy": [7]},
|
{"id": 9, "planTask": 4, "subject": "Task 4: MxGatewayEndpointConfigValidator + tests", "status": "pending", "blockedBy": [7]},
|
||||||
{"id": 10, "planTask": 5, "subject": "Task 5: Client seam interfaces + MxGatewayGlobalOptions", "status": "pending"},
|
{"id": 10, "planTask": 5, "subject": "Task 5: Client seam interfaces + MxGatewayGlobalOptions", "status": "completed"},
|
||||||
{"id": 11, "planTask": 6, "subject": "Task 6: Adapter connect/disconnect/Disconnected + value mapping", "status": "pending", "blockedBy": [7, 10]},
|
{"id": 11, "planTask": 6, "subject": "Task 6: Adapter connect/disconnect/Disconnected + value mapping", "status": "pending", "blockedBy": [7, 10]},
|
||||||
{"id": 12, "planTask": 7, "subject": "Task 7: Adapter subscribe/unsubscribe + event routing", "status": "pending", "blockedBy": [11]},
|
{"id": 12, "planTask": 7, "subject": "Task 7: Adapter subscribe/unsubscribe + event routing", "status": "pending", "blockedBy": [11]},
|
||||||
{"id": 13, "planTask": 8, "subject": "Task 8: Adapter read/write batch + error classification", "status": "pending", "blockedBy": [11]},
|
{"id": 13, "planTask": 8, "subject": "Task 8: Adapter read/write batch + error classification", "status": "pending", "blockedBy": [11]},
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public static class ServiceCollectionExtensions
|
|||||||
// Backs the Audit Log page's Export button via GET /api/centralui/audit/export.
|
// Backs the Audit Log page's Export button via GET /api/centralui/audit/export.
|
||||||
services.AddScoped<IAuditLogExportService, AuditLogExportService>();
|
services.AddScoped<IAuditLogExportService, AuditLogExportService>();
|
||||||
|
|
||||||
// OPC UA Tag Browser (Task 14): facade over CommunicationService.BrowseOpcUaNodeAsync
|
// OPC UA Tag Browser (Task 14): facade over CommunicationService.BrowseNodeAsync
|
||||||
// that enforces the CentralUI-side Design-role trust boundary and translates
|
// that enforces the CentralUI-side Design-role trust boundary and translates
|
||||||
// transport failures into typed BrowseFailure results for the dialog.
|
// transport failures into typed BrowseFailure results for the dialog.
|
||||||
services.AddScoped<IOpcUaBrowseService, OpcUaBrowseService>();
|
services.AddScoped<IOpcUaBrowseService, OpcUaBrowseService>();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
|||||||
/// CentralUI facade over the central-to-site OPC UA browse command. Backs the
|
/// CentralUI facade over the central-to-site OPC UA browse command. Backs the
|
||||||
/// OPC UA Tag Browser dialog: each tree expansion / manual node-id paste calls
|
/// OPC UA Tag Browser dialog: each tree expansion / manual node-id paste calls
|
||||||
/// <see cref="BrowseChildrenAsync"/>, which forwards a
|
/// <see cref="BrowseChildrenAsync"/>, which forwards a
|
||||||
/// <see cref="BrowseOpcUaNodeCommand"/> to the owning site via
|
/// <see cref="BrowseNodeCommand"/> to the owning site via
|
||||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Communication.CommunicationService"/>.
|
/// <see cref="ZB.MOM.WW.ScadaBridge.Communication.CommunicationService"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
@@ -29,7 +29,7 @@ public interface IOpcUaBrowseService
|
|||||||
/// <param name="connectionName">Name of the site-local data connection to browse against — the site's <c>DataConnectionManagerActor</c> indexes its children by name.</param>
|
/// <param name="connectionName">Name of the site-local data connection to browse against — the site's <c>DataConnectionManagerActor</c> indexes its children by name.</param>
|
||||||
/// <param name="parentNodeId">Node to browse, or <c>null</c> to browse from the server root.</param>
|
/// <param name="parentNodeId">Node to browse, or <c>null</c> to browse from the server root.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task<BrowseOpcUaNodeResult> BrowseChildrenAsync(
|
Task<BrowseNodeResult> BrowseChildrenAsync(
|
||||||
string siteId,
|
string siteId,
|
||||||
string connectionName,
|
string connectionName,
|
||||||
string? parentNodeId,
|
string? parentNodeId,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default <see cref="IOpcUaBrowseService"/> implementation — a thin facade over
|
/// Default <see cref="IOpcUaBrowseService"/> implementation — a thin facade over
|
||||||
/// <see cref="CommunicationService.BrowseOpcUaNodeAsync"/> that enforces the
|
/// <see cref="CommunicationService.BrowseNodeAsync"/> that enforces the
|
||||||
/// CentralUI-side <c>Design</c>-role trust boundary and translates transport
|
/// CentralUI-side <c>Design</c>-role trust boundary and translates transport
|
||||||
/// exceptions into a typed <see cref="BrowseFailure"/> result.
|
/// exceptions into a typed <see cref="BrowseFailure"/> result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -36,7 +36,7 @@ public sealed class OpcUaBrowseService : IOpcUaBrowseService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task<BrowseOpcUaNodeResult> BrowseChildrenAsync(
|
public async Task<BrowseNodeResult> BrowseChildrenAsync(
|
||||||
string siteId,
|
string siteId,
|
||||||
string connectionName,
|
string connectionName,
|
||||||
string? parentNodeId,
|
string? parentNodeId,
|
||||||
@@ -47,7 +47,7 @@ public sealed class OpcUaBrowseService : IOpcUaBrowseService
|
|||||||
var state = await _auth.GetAuthenticationStateAsync();
|
var state = await _auth.GetAuthenticationStateAsync();
|
||||||
if (!state.User.HasClaim(JwtTokenService.RoleClaimType, "Design"))
|
if (!state.User.HasClaim(JwtTokenService.RoleClaimType, "Design"))
|
||||||
{
|
{
|
||||||
return new BrowseOpcUaNodeResult(
|
return new BrowseNodeResult(
|
||||||
Array.Empty<BrowseNode>(),
|
Array.Empty<BrowseNode>(),
|
||||||
Truncated: false,
|
Truncated: false,
|
||||||
new BrowseFailure(BrowseFailureKind.ServerError, "Not authorized."));
|
new BrowseFailure(BrowseFailureKind.ServerError, "Not authorized."));
|
||||||
@@ -55,9 +55,9 @@ public sealed class OpcUaBrowseService : IOpcUaBrowseService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await _communication.BrowseOpcUaNodeAsync(
|
return await _communication.BrowseNodeAsync(
|
||||||
siteId,
|
siteId,
|
||||||
new BrowseOpcUaNodeCommand(connectionName, parentNodeId),
|
new BrowseNodeCommand(connectionName, parentNodeId),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
catch (TimeoutException ex)
|
catch (TimeoutException ex)
|
||||||
@@ -65,7 +65,7 @@ public sealed class OpcUaBrowseService : IOpcUaBrowseService
|
|||||||
// Akka Ask timed out — the site (or its OPC UA session) didn't answer
|
// Akka Ask timed out — the site (or its OPC UA session) didn't answer
|
||||||
// within CommunicationOptions.QueryTimeout. Surface as a typed
|
// within CommunicationOptions.QueryTimeout. Surface as a typed
|
||||||
// Timeout failure so the dialog can render an inline banner.
|
// Timeout failure so the dialog can render an inline banner.
|
||||||
return new BrowseOpcUaNodeResult(
|
return new BrowseNodeResult(
|
||||||
Array.Empty<BrowseNode>(),
|
Array.Empty<BrowseNode>(),
|
||||||
Truncated: false,
|
Truncated: false,
|
||||||
new BrowseFailure(BrowseFailureKind.Timeout, ex.Message));
|
new BrowseFailure(BrowseFailureKind.Timeout, ex.Message));
|
||||||
@@ -80,7 +80,7 @@ public sealed class OpcUaBrowseService : IOpcUaBrowseService
|
|||||||
{
|
{
|
||||||
// Any other transport / serialization failure: keep the dialog
|
// Any other transport / serialization failure: keep the dialog
|
||||||
// alive and let the user fall back to manual node-id paste.
|
// alive and let the user fall back to manual node-id paste.
|
||||||
return new BrowseOpcUaNodeResult(
|
return new BrowseNodeResult(
|
||||||
Array.Empty<BrowseNode>(),
|
Array.Empty<BrowseNode>(),
|
||||||
Truncated: false,
|
Truncated: false,
|
||||||
new BrowseFailure(BrowseFailureKind.ServerError, ex.Message));
|
new BrowseFailure(BrowseFailureKind.ServerError, ex.Message));
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="ConnectionName">Name of the site-local data connection to browse against.</param>
|
/// <param name="ConnectionName">Name of the site-local data connection to browse against.</param>
|
||||||
/// <param name="ParentNodeId">Node to browse, or null to browse from the server root (ObjectsFolder).</param>
|
/// <param name="ParentNodeId">Node to browse, or null to browse from the server root (ObjectsFolder).</param>
|
||||||
public record BrowseOpcUaNodeCommand(
|
public record BrowseNodeCommand(
|
||||||
string ConnectionName,
|
string ConnectionName,
|
||||||
string? ParentNodeId);
|
string? ParentNodeId);
|
||||||
|
|
||||||
public record BrowseOpcUaNodeResult(
|
public record BrowseNodeResult(
|
||||||
IReadOnlyList<BrowseNode> Children,
|
IReadOnlyList<BrowseNode> Children,
|
||||||
bool Truncated,
|
bool Truncated,
|
||||||
BrowseFailure? Failure);
|
BrowseFailure? Failure);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Keyed by <see cref="ConnectionName"/> (not id) for the same reason as
|
/// Keyed by <see cref="ConnectionName"/> (not id) for the same reason as
|
||||||
/// <see cref="BrowseOpcUaNodeCommand"/>: the site-side
|
/// <see cref="BrowseNodeCommand"/>: the site-side
|
||||||
/// <c>DataConnectionManagerActor</c> indexes its children by connection name,
|
/// <c>DataConnectionManagerActor</c> indexes its children by connection name,
|
||||||
/// and the central UI already has the connection name in scope from the
|
/// and the central UI already has the connection name in scope from the
|
||||||
/// bindings table. The central <c>DataConnections</c> table's id is not
|
/// bindings table. The central <c>DataConnections</c> table's id is not
|
||||||
|
|||||||
@@ -152,10 +152,10 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
|
|||||||
// DataConnectionActor children (which own the live OPC UA sessions)
|
// DataConnectionActor children (which own the live OPC UA sessions)
|
||||||
// only exist on the singleton's node. The singleton then re-forwards
|
// only exist on the singleton's node. The singleton then re-forwards
|
||||||
// to its own /user/dcl-manager, which DOES have the connection.
|
// to its own /user/dcl-manager, which DOES have the connection.
|
||||||
Receive<BrowseOpcUaNodeCommand>(msg => _deploymentManagerProxy.Forward(msg));
|
Receive<BrowseNodeCommand>(msg => _deploymentManagerProxy.Forward(msg));
|
||||||
|
|
||||||
// Test Bindings (interactive design-time read) — same routing rationale
|
// Test Bindings (interactive design-time read) — same routing rationale
|
||||||
// as BrowseOpcUaNodeCommand above: the singleton always lands on the
|
// as BrowseNodeCommand above: the singleton always lands on the
|
||||||
// active site node, which is the node that owns the DataConnectionActor
|
// active site node, which is the node that owns the DataConnectionActor
|
||||||
// children holding the live OPC UA sessions.
|
// children holding the live OPC UA sessions.
|
||||||
Receive<ReadTagValuesCommand>(msg => _deploymentManagerProxy.Forward(msg));
|
Receive<ReadTagValuesCommand>(msg => _deploymentManagerProxy.Forward(msg));
|
||||||
|
|||||||
@@ -360,13 +360,13 @@ public class CommunicationService
|
|||||||
/// <param name="command">The OPC UA browse command.</param>
|
/// <param name="command">The OPC UA browse command.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>The browse result (children + truncation flag + structured failure).</returns>
|
/// <returns>The browse result (children + truncation flag + structured failure).</returns>
|
||||||
public Task<BrowseOpcUaNodeResult> BrowseOpcUaNodeAsync(
|
public Task<BrowseNodeResult> BrowseNodeAsync(
|
||||||
string siteId,
|
string siteId,
|
||||||
BrowseOpcUaNodeCommand command,
|
BrowseNodeCommand command,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var envelope = new SiteEnvelope(siteId, command);
|
var envelope = new SiteEnvelope(siteId, command);
|
||||||
return GetActor().Ask<BrowseOpcUaNodeResult>(
|
return GetActor().Ask<BrowseNodeResult>(
|
||||||
envelope, _options.QueryTimeout, cancellationToken);
|
envelope, _options.QueryTimeout, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +377,7 @@ public class CommunicationService
|
|||||||
/// server backing the given data connection. Used by the CentralUI "Test
|
/// server backing the given data connection. Used by the CentralUI "Test
|
||||||
/// Bindings" dialog on the Configure Instance page. The Ask is bounded by
|
/// Bindings" dialog on the Configure Instance page. The Ask is bounded by
|
||||||
/// <see cref="CommunicationOptions.QueryTimeout"/> — same latency budget
|
/// <see cref="CommunicationOptions.QueryTimeout"/> — same latency budget
|
||||||
/// as <see cref="BrowseOpcUaNodeAsync"/> (both are interactive one-shot
|
/// as <see cref="BrowseNodeAsync"/> (both are interactive one-shot
|
||||||
/// design-time queries).
|
/// design-time queries).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="siteId">The target site identifier.</param>
|
/// <param name="siteId">The target site identifier.</param>
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
|||||||
// apply it so its state survives into the next ReSubscribeAll.
|
// apply it so its state survives into the next ReSubscribeAll.
|
||||||
HandleSubscribeCompleted(sc);
|
HandleSubscribeCompleted(sc);
|
||||||
break;
|
break;
|
||||||
case BrowseOpcUaNodeCommand browse:
|
case BrowseNodeCommand browse:
|
||||||
// Browse is an interactive design-time query; never stash. The
|
// Browse is an interactive design-time query; never stash. The
|
||||||
// adapter has no session yet in this state, so reply with a
|
// adapter has no session yet in this state, so reply with a
|
||||||
// typed ConnectionNotConnected failure so the dialog can render
|
// typed ConnectionNotConnected failure so the dialog can render
|
||||||
@@ -307,7 +307,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
|||||||
case RetryTagResolution:
|
case RetryTagResolution:
|
||||||
HandleRetryTagResolution();
|
HandleRetryTagResolution();
|
||||||
break;
|
break;
|
||||||
case BrowseOpcUaNodeCommand browse:
|
case BrowseNodeCommand browse:
|
||||||
HandleBrowse(browse);
|
HandleBrowse(browse);
|
||||||
break;
|
break;
|
||||||
case ReadTagValuesCommand read:
|
case ReadTagValuesCommand read:
|
||||||
@@ -432,7 +432,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
|||||||
// apply it so its state survives into the next ReSubscribeAll.
|
// apply it so its state survives into the next ReSubscribeAll.
|
||||||
HandleSubscribeCompleted(sc);
|
HandleSubscribeCompleted(sc);
|
||||||
break;
|
break;
|
||||||
case BrowseOpcUaNodeCommand browse:
|
case BrowseNodeCommand browse:
|
||||||
// Browse is design-time and never stashed. While reconnecting
|
// Browse is design-time and never stashed. While reconnecting
|
||||||
// the adapter has no live session, so the adapter call will
|
// the adapter has no live session, so the adapter call will
|
||||||
// throw ConnectionNotConnectedException — mapped by HandleBrowse.
|
// throw ConnectionNotConnectedException — mapped by HandleBrowse.
|
||||||
@@ -982,7 +982,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
|||||||
// ── OPC UA Tag Browser (interactive design-time query) ──
|
// ── OPC UA Tag Browser (interactive design-time query) ──
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles a <see cref="BrowseOpcUaNodeCommand"/> forwarded by the
|
/// Handles a <see cref="BrowseNodeCommand"/> forwarded by the
|
||||||
/// <see cref="DataConnectionManagerActor"/>. The capability check (does
|
/// <see cref="DataConnectionManagerActor"/>. The capability check (does
|
||||||
/// this adapter support browsing?) and all browse-failure mapping live
|
/// this adapter support browsing?) and all browse-failure mapping live
|
||||||
/// here because the adapter is held by this actor, not the manager.
|
/// here because the adapter is held by this actor, not the manager.
|
||||||
@@ -999,14 +999,14 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
|||||||
/// <see cref="HandleWrite"/> — so the captured <see cref="Sender"/> is
|
/// <see cref="HandleWrite"/> — so the captured <see cref="Sender"/> is
|
||||||
/// safe to use from the continuation (which runs off the actor thread).
|
/// safe to use from the continuation (which runs off the actor thread).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void HandleBrowse(BrowseOpcUaNodeCommand command)
|
private void HandleBrowse(BrowseNodeCommand command)
|
||||||
{
|
{
|
||||||
var sender = Sender;
|
var sender = Sender;
|
||||||
|
|
||||||
if (_adapter is not IBrowsableDataConnection browsable)
|
if (_adapter is not IBrowsableDataConnection browsable)
|
||||||
{
|
{
|
||||||
_log.Debug("[{0}] Browse requested but adapter does not implement IBrowsableDataConnection", _connectionName);
|
_log.Debug("[{0}] Browse requested but adapter does not implement IBrowsableDataConnection", _connectionName);
|
||||||
sender.Tell(new BrowseOpcUaNodeResult(
|
sender.Tell(new BrowseNodeResult(
|
||||||
Array.Empty<BrowseNode>(),
|
Array.Empty<BrowseNode>(),
|
||||||
Truncated: false,
|
Truncated: false,
|
||||||
new BrowseFailure(
|
new BrowseFailure(
|
||||||
@@ -1021,21 +1021,21 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
|||||||
{
|
{
|
||||||
if (t.IsCompletedSuccessfully)
|
if (t.IsCompletedSuccessfully)
|
||||||
{
|
{
|
||||||
return new BrowseOpcUaNodeResult(t.Result.Children, t.Result.Truncated, Failure: null);
|
return new BrowseNodeResult(t.Result.Children, t.Result.Truncated, Failure: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var baseEx = t.Exception?.GetBaseException();
|
var baseEx = t.Exception?.GetBaseException();
|
||||||
return baseEx switch
|
return baseEx switch
|
||||||
{
|
{
|
||||||
ConnectionNotConnectedException notConnected => new BrowseOpcUaNodeResult(
|
ConnectionNotConnectedException notConnected => new BrowseNodeResult(
|
||||||
Array.Empty<BrowseNode>(),
|
Array.Empty<BrowseNode>(),
|
||||||
Truncated: false,
|
Truncated: false,
|
||||||
new BrowseFailure(BrowseFailureKind.ConnectionNotConnected, notConnected.Message)),
|
new BrowseFailure(BrowseFailureKind.ConnectionNotConnected, notConnected.Message)),
|
||||||
OperationCanceledException => new BrowseOpcUaNodeResult(
|
OperationCanceledException => new BrowseNodeResult(
|
||||||
Array.Empty<BrowseNode>(),
|
Array.Empty<BrowseNode>(),
|
||||||
Truncated: false,
|
Truncated: false,
|
||||||
new BrowseFailure(BrowseFailureKind.Timeout, "Browse cancelled.")),
|
new BrowseFailure(BrowseFailureKind.Timeout, "Browse cancelled.")),
|
||||||
_ => new BrowseOpcUaNodeResult(
|
_ => new BrowseNodeResult(
|
||||||
Array.Empty<BrowseNode>(),
|
Array.Empty<BrowseNode>(),
|
||||||
Truncated: false,
|
Truncated: false,
|
||||||
new BrowseFailure(
|
new BrowseFailure(
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class DataConnectionManagerActor : ReceiveActor
|
|||||||
Receive<WriteTagRequest>(HandleRouteWrite);
|
Receive<WriteTagRequest>(HandleRouteWrite);
|
||||||
Receive<RemoveConnectionCommand>(HandleRemoveConnection);
|
Receive<RemoveConnectionCommand>(HandleRemoveConnection);
|
||||||
Receive<GetAllHealthReports>(HandleGetAllHealthReports);
|
Receive<GetAllHealthReports>(HandleGetAllHealthReports);
|
||||||
Receive<BrowseOpcUaNodeCommand>(HandleBrowse);
|
Receive<BrowseNodeCommand>(HandleBrowse);
|
||||||
Receive<ReadTagValuesCommand>(HandleReadTagValues);
|
Receive<ReadTagValuesCommand>(HandleReadTagValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ public class DataConnectionManagerActor : ReceiveActor
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Routes a <see cref="BrowseOpcUaNodeCommand"/> from the central UI's OPC UA
|
/// Routes a <see cref="BrowseNodeCommand"/> from the central UI's OPC UA
|
||||||
/// Tag Browser to the child <see cref="DataConnectionActor"/> that owns the
|
/// Tag Browser to the child <see cref="DataConnectionActor"/> that owns the
|
||||||
/// named connection. The manager is the only actor that knows whether a
|
/// named connection. The manager is the only actor that knows whether a
|
||||||
/// connection exists at this site — so it owns the
|
/// connection exists at this site — so it owns the
|
||||||
@@ -123,7 +123,7 @@ public class DataConnectionManagerActor : ReceiveActor
|
|||||||
/// else (capability check, session state, server errors) lives inside the
|
/// else (capability check, session state, server errors) lives inside the
|
||||||
/// child where the adapter is held.
|
/// child where the adapter is held.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void HandleBrowse(BrowseOpcUaNodeCommand command)
|
private void HandleBrowse(BrowseNodeCommand command)
|
||||||
{
|
{
|
||||||
if (_connectionActors.TryGetValue(command.ConnectionName, out var actor))
|
if (_connectionActors.TryGetValue(command.ConnectionName, out var actor))
|
||||||
{
|
{
|
||||||
@@ -132,7 +132,7 @@ public class DataConnectionManagerActor : ReceiveActor
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
_log.Warning("No connection actor for {0} during browse", command.ConnectionName);
|
_log.Warning("No connection actor for {0} during browse", command.ConnectionName);
|
||||||
Sender.Tell(new BrowseOpcUaNodeResult(
|
Sender.Tell(new BrowseNodeResult(
|
||||||
Array.Empty<BrowseNode>(),
|
Array.Empty<BrowseNode>(),
|
||||||
Truncated: false,
|
Truncated: false,
|
||||||
new BrowseFailure(
|
new BrowseFailure(
|
||||||
|
|||||||
@@ -149,12 +149,12 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
|||||||
Receive<RouteToSetAttributesRequest>(RouteInboundApiSetAttributes);
|
Receive<RouteToSetAttributesRequest>(RouteInboundApiSetAttributes);
|
||||||
|
|
||||||
// OPC UA Tag Browser — singleton-only re-forward to local /user/dcl-manager.
|
// OPC UA Tag Browser — singleton-only re-forward to local /user/dcl-manager.
|
||||||
// BrowseOpcUaNodeCommand is routed to this singleton (active node) by
|
// BrowseNodeCommand is routed to this singleton (active node) by
|
||||||
// SiteCommunicationActor so the dcl-manager we forward to is guaranteed
|
// SiteCommunicationActor so the dcl-manager we forward to is guaranteed
|
||||||
// to be the one holding the live DataConnectionActor children. ActorSelection
|
// to be the one holding the live DataConnectionActor children. ActorSelection
|
||||||
// has no Forward() extension in this Akka.NET version, so we Tell with the
|
// has no Forward() extension in this Akka.NET version, so we Tell with the
|
||||||
// original Sender preserved (semantically identical to Forward).
|
// original Sender preserved (semantically identical to Forward).
|
||||||
Receive<BrowseOpcUaNodeCommand>(msg =>
|
Receive<BrowseNodeCommand>(msg =>
|
||||||
Context.ActorSelection("/user/dcl-manager").Tell(msg, Sender));
|
Context.ActorSelection("/user/dcl-manager").Tell(msg, Sender));
|
||||||
|
|
||||||
// Test Bindings — same singleton-only re-forward as the browse handler
|
// Test Bindings — same singleton-only re-forward as the browse handler
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|||||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Verifies that <see cref="BrowseOpcUaNodeCommand"/> is discovered by
|
/// Verifies that <see cref="BrowseNodeCommand"/> is discovered by
|
||||||
/// <see cref="ManagementCommandRegistry"/> so it travels over the management
|
/// <see cref="ManagementCommandRegistry"/> so it travels over the management
|
||||||
/// boundary as a known command (resolvable by wire name and round-trippable
|
/// boundary as a known command (resolvable by wire name and round-trippable
|
||||||
/// through <c>GetCommandName</c> / <c>Resolve</c>).
|
/// through <c>GetCommandName</c> / <c>Resolve</c>).
|
||||||
@@ -11,13 +11,13 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
|||||||
public class BrowseCommandsRegistryTests
|
public class BrowseCommandsRegistryTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Registry_discovers_BrowseOpcUaNodeCommand()
|
public void Registry_discovers_BrowseNodeCommand()
|
||||||
{
|
{
|
||||||
// GetCommandName throws ArgumentException for any type the registry
|
// GetCommandName throws ArgumentException for any type the registry
|
||||||
// does not contain, so a successful call here is proof of discovery.
|
// does not contain, so a successful call here is proof of discovery.
|
||||||
var name = ManagementCommandRegistry.GetCommandName(typeof(BrowseOpcUaNodeCommand));
|
var name = ManagementCommandRegistry.GetCommandName(typeof(BrowseNodeCommand));
|
||||||
|
|
||||||
Assert.Equal("BrowseOpcUaNode", name);
|
Assert.Equal("BrowseNode", name);
|
||||||
Assert.Equal(typeof(BrowseOpcUaNodeCommand), ManagementCommandRegistry.Resolve(name));
|
Assert.Equal(typeof(BrowseNodeCommand), ManagementCommandRegistry.Resolve(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-9
@@ -14,7 +14,7 @@ namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Actors;
|
|||||||
/// Task 10 (opcua-tag-browser): the site-side
|
/// Task 10 (opcua-tag-browser): the site-side
|
||||||
/// <see cref="DataConnectionManagerActor"/> + child
|
/// <see cref="DataConnectionManagerActor"/> + child
|
||||||
/// <see cref="DataConnectionActor"/> together resolve
|
/// <see cref="DataConnectionActor"/> together resolve
|
||||||
/// <see cref="BrowseOpcUaNodeCommand"/> against the live adapter and surface
|
/// <see cref="BrowseNodeCommand"/> against the live adapter and surface
|
||||||
/// every browse outcome as a typed <see cref="BrowseFailure"/>. The split is:
|
/// every browse outcome as a typed <see cref="BrowseFailure"/>. The split is:
|
||||||
/// the manager owns <see cref="BrowseFailureKind.ConnectionNotFound"/> (only it
|
/// the manager owns <see cref="BrowseFailureKind.ConnectionNotFound"/> (only it
|
||||||
/// knows the per-site connection set); everything else lives in the child where
|
/// knows the per-site connection set); everything else lives in the child where
|
||||||
@@ -50,9 +50,9 @@ public class DataConnectionManagerBrowseHandlerTests : TestKit
|
|||||||
// No CreateConnectionCommand sent — the manager has zero children, so a
|
// No CreateConnectionCommand sent — the manager has zero children, so a
|
||||||
// browse against any name must be rejected with ConnectionNotFound
|
// browse against any name must be rejected with ConnectionNotFound
|
||||||
// (the manager is the only actor with site-level visibility).
|
// (the manager is the only actor with site-level visibility).
|
||||||
manager.Tell(new BrowseOpcUaNodeCommand("unknown-connection", ParentNodeId: null));
|
manager.Tell(new BrowseNodeCommand("unknown-connection", ParentNodeId: null));
|
||||||
|
|
||||||
var reply = ExpectMsg<BrowseOpcUaNodeResult>();
|
var reply = ExpectMsg<BrowseNodeResult>();
|
||||||
Assert.NotNull(reply.Failure);
|
Assert.NotNull(reply.Failure);
|
||||||
Assert.Equal(BrowseFailureKind.ConnectionNotFound, reply.Failure!.Kind);
|
Assert.Equal(BrowseFailureKind.ConnectionNotFound, reply.Failure!.Kind);
|
||||||
Assert.Empty(reply.Children);
|
Assert.Empty(reply.Children);
|
||||||
@@ -80,9 +80,9 @@ public class DataConnectionManagerBrowseHandlerTests : TestKit
|
|||||||
() => _factory.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Create"),
|
() => _factory.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Create"),
|
||||||
TimeSpan.FromSeconds(2));
|
TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
manager.Tell(new BrowseOpcUaNodeCommand("conn-bare", ParentNodeId: null));
|
manager.Tell(new BrowseNodeCommand("conn-bare", ParentNodeId: null));
|
||||||
|
|
||||||
var reply = ExpectMsg<BrowseOpcUaNodeResult>(TimeSpan.FromSeconds(3));
|
var reply = ExpectMsg<BrowseNodeResult>(TimeSpan.FromSeconds(3));
|
||||||
Assert.NotNull(reply.Failure);
|
Assert.NotNull(reply.Failure);
|
||||||
Assert.Equal(BrowseFailureKind.NotBrowsable, reply.Failure!.Kind);
|
Assert.Equal(BrowseFailureKind.NotBrowsable, reply.Failure!.Kind);
|
||||||
Assert.Empty(reply.Children);
|
Assert.Empty(reply.Children);
|
||||||
@@ -120,9 +120,9 @@ public class DataConnectionManagerBrowseHandlerTests : TestKit
|
|||||||
() => _factory.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Create"),
|
() => _factory.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Create"),
|
||||||
TimeSpan.FromSeconds(2));
|
TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
manager.Tell(new BrowseOpcUaNodeCommand("conn-ok", ParentNodeId: null));
|
manager.Tell(new BrowseNodeCommand("conn-ok", ParentNodeId: null));
|
||||||
|
|
||||||
var reply = ExpectMsg<BrowseOpcUaNodeResult>(TimeSpan.FromSeconds(3));
|
var reply = ExpectMsg<BrowseNodeResult>(TimeSpan.FromSeconds(3));
|
||||||
Assert.Null(reply.Failure);
|
Assert.Null(reply.Failure);
|
||||||
Assert.Equal(2, reply.Children.Count);
|
Assert.Equal(2, reply.Children.Count);
|
||||||
Assert.Equal("ns=2;s=A", reply.Children[0].NodeId);
|
Assert.Equal("ns=2;s=A", reply.Children[0].NodeId);
|
||||||
@@ -155,9 +155,9 @@ public class DataConnectionManagerBrowseHandlerTests : TestKit
|
|||||||
() => _factory.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Create"),
|
() => _factory.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Create"),
|
||||||
TimeSpan.FromSeconds(2));
|
TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
manager.Tell(new BrowseOpcUaNodeCommand("conn-down", ParentNodeId: null));
|
manager.Tell(new BrowseNodeCommand("conn-down", ParentNodeId: null));
|
||||||
|
|
||||||
var reply = ExpectMsg<BrowseOpcUaNodeResult>(TimeSpan.FromSeconds(3));
|
var reply = ExpectMsg<BrowseNodeResult>(TimeSpan.FromSeconds(3));
|
||||||
Assert.NotNull(reply.Failure);
|
Assert.NotNull(reply.Failure);
|
||||||
Assert.Equal(BrowseFailureKind.ConnectionNotConnected, reply.Failure!.Kind);
|
Assert.Equal(BrowseFailureKind.ConnectionNotConnected, reply.Failure!.Kind);
|
||||||
Assert.Empty(reply.Children);
|
Assert.Empty(reply.Children);
|
||||||
|
|||||||
Reference in New Issue
Block a user