feat: OPC UA address-space search plumbing — actor + comm + BrowseService (T15)
This commit is contained in:
@@ -269,6 +269,12 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
// an inline banner.
|
||||
HandleBrowse(browse);
|
||||
break;
|
||||
case SearchAddressSpaceCommand search:
|
||||
// Search is the address-space analogue of browse — same rule:
|
||||
// never stash; the adapter has no session yet here, so
|
||||
// HandleSearch short-circuits to ConnectionNotConnected.
|
||||
HandleSearch(search);
|
||||
break;
|
||||
case ReadTagValuesCommand read:
|
||||
// Same rule as browse — never stash; adapter is not yet
|
||||
// connected, so HandleReadTagValues short-circuits to
|
||||
@@ -353,6 +359,9 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
case BrowseNodeCommand browse:
|
||||
HandleBrowse(browse);
|
||||
break;
|
||||
case SearchAddressSpaceCommand search:
|
||||
HandleSearch(search);
|
||||
break;
|
||||
case ReadTagValuesCommand read:
|
||||
HandleReadTagValues(read);
|
||||
break;
|
||||
@@ -497,6 +506,12 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
// throw ConnectionNotConnectedException — mapped by HandleBrowse.
|
||||
HandleBrowse(browse);
|
||||
break;
|
||||
case SearchAddressSpaceCommand search:
|
||||
// Same rule as browse — never stashed; while reconnecting the
|
||||
// adapter is not Connected so the adapter call throws
|
||||
// ConnectionNotConnectedException, mapped by HandleSearch.
|
||||
HandleSearch(search);
|
||||
break;
|
||||
case ReadTagValuesCommand read:
|
||||
// Same rule as browse — never stashed; while reconnecting the
|
||||
// adapter is not Connected so HandleReadTagValues short-circuits
|
||||
@@ -1203,6 +1218,83 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
}).PipeTo(sender);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a <see cref="SearchAddressSpaceCommand"/> forwarded by the
|
||||
/// <see cref="DataConnectionManagerActor"/> — the address-space analogue of
|
||||
/// <see cref="HandleBrowse"/>. The capability check (does this adapter support
|
||||
/// search?) and all failure mapping live here because the adapter is held by
|
||||
/// this actor, not the manager. The search path reuses the browse
|
||||
/// <see cref="BrowseFailure"/> kinds rather than inventing a parallel set.
|
||||
///
|
||||
/// Failure mapping:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="BrowseFailureKind.NotBrowsable"/> — adapter is not <see cref="IAddressSpaceSearchable"/>, or it threw <see cref="NotSupportedException"/> (searchable adapter, but the server/protocol cannot search); message carried verbatim in the latter case.</item>
|
||||
/// <item><see cref="BrowseFailureKind.ConnectionNotConnected"/> — adapter threw <see cref="ConnectionNotConnectedException"/>.</item>
|
||||
/// <item><see cref="BrowseFailureKind.Timeout"/> — adapter threw <see cref="OperationCanceledException"/>.</item>
|
||||
/// <item><see cref="BrowseFailureKind.ServerError"/> — any other exception, message carried verbatim.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// The adapter already caps the match list at <c>MaxResults</c>, so unlike
|
||||
/// <see cref="HandleBrowse"/> there is no frame-budget clip here — the bound is
|
||||
/// supplied by the caller (B6) and chosen to stay well under Akka's remote
|
||||
/// frame size. The reply is sent via <c>PipeTo(sender)</c>, the same pattern
|
||||
/// used by <see cref="HandleBrowse"/>.
|
||||
/// </summary>
|
||||
private void HandleSearch(SearchAddressSpaceCommand command)
|
||||
{
|
||||
var sender = Sender;
|
||||
|
||||
if (_adapter is not IAddressSpaceSearchable searchable)
|
||||
{
|
||||
_log.Debug("[{0}] Search requested but adapter does not implement IAddressSpaceSearchable", _connectionName);
|
||||
sender.Tell(new SearchAddressSpaceResult(
|
||||
Array.Empty<AddressSpaceMatch>(),
|
||||
CapReached: false,
|
||||
new BrowseFailure(
|
||||
BrowseFailureKind.NotBrowsable,
|
||||
$"Connection '{_connectionName}' does not support search.")));
|
||||
return;
|
||||
}
|
||||
|
||||
_log.Debug("[{0}] Searching address space for '{1}' (maxDepth={2}, maxResults={3})",
|
||||
_connectionName, command.Query, command.MaxDepth, command.MaxResults);
|
||||
|
||||
searchable.SearchAddressSpaceAsync(command.Query, command.MaxDepth, command.MaxResults).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsCompletedSuccessfully)
|
||||
{
|
||||
// The adapter already bounded the walk by MaxResults — pass the
|
||||
// matches and the cap flag straight through.
|
||||
return new SearchAddressSpaceResult(t.Result.Matches, t.Result.CapReached, Failure: null);
|
||||
}
|
||||
|
||||
var baseEx = t.Exception?.GetBaseException();
|
||||
return baseEx switch
|
||||
{
|
||||
ConnectionNotConnectedException notConnected => new SearchAddressSpaceResult(
|
||||
Array.Empty<AddressSpaceMatch>(),
|
||||
CapReached: false,
|
||||
new BrowseFailure(BrowseFailureKind.ConnectionNotConnected, notConnected.Message)),
|
||||
OperationCanceledException => new SearchAddressSpaceResult(
|
||||
Array.Empty<AddressSpaceMatch>(),
|
||||
CapReached: false,
|
||||
new BrowseFailure(BrowseFailureKind.Timeout, "Search cancelled.")),
|
||||
// Adapter reachable but the protocol/server cannot search. Carry
|
||||
// the adapter's explanatory message through as NotBrowsable.
|
||||
NotSupportedException notSupported => new SearchAddressSpaceResult(
|
||||
Array.Empty<AddressSpaceMatch>(),
|
||||
CapReached: false,
|
||||
new BrowseFailure(BrowseFailureKind.NotBrowsable, notSupported.Message)),
|
||||
_ => new SearchAddressSpaceResult(
|
||||
Array.Empty<AddressSpaceMatch>(),
|
||||
CapReached: false,
|
||||
new BrowseFailure(
|
||||
BrowseFailureKind.ServerError,
|
||||
baseEx?.Message ?? "Unknown search error.")),
|
||||
};
|
||||
}).PipeTo(sender);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimated-byte ceiling for a single <see cref="BrowseNodeResult"/>, kept
|
||||
/// comfortably below Akka's default 128 KB remote frame size. A browse reply
|
||||
|
||||
@@ -50,6 +50,7 @@ public class DataConnectionManagerActor : ReceiveActor
|
||||
Receive<RemoveConnectionCommand>(HandleRemoveConnection);
|
||||
Receive<GetAllHealthReports>(HandleGetAllHealthReports);
|
||||
Receive<BrowseNodeCommand>(HandleBrowse);
|
||||
Receive<SearchAddressSpaceCommand>(HandleSearch);
|
||||
Receive<ReadTagValuesCommand>(HandleReadTagValues);
|
||||
}
|
||||
|
||||
@@ -189,6 +190,32 @@ public class DataConnectionManagerActor : ReceiveActor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Routes a <see cref="SearchAddressSpaceCommand"/> from the central UI's OPC
|
||||
/// UA tag picker to the child <see cref="DataConnectionActor"/> that owns the
|
||||
/// named connection — the address-space analogue of <see cref="HandleBrowse"/>.
|
||||
/// Same split: the manager owns <see cref="BrowseFailureKind.ConnectionNotFound"/>
|
||||
/// (only it knows the per-site connection set); the capability check and every
|
||||
/// other failure live inside the child where the adapter is held.
|
||||
/// </summary>
|
||||
private void HandleSearch(SearchAddressSpaceCommand command)
|
||||
{
|
||||
if (_connectionActors.TryGetValue(command.ConnectionName, out var actor))
|
||||
{
|
||||
actor.Forward(command);
|
||||
}
|
||||
else
|
||||
{
|
||||
_log.Warning("No connection actor for {0} during search", command.ConnectionName);
|
||||
Sender.Tell(new SearchAddressSpaceResult(
|
||||
Array.Empty<AddressSpaceMatch>(),
|
||||
CapReached: false,
|
||||
new BrowseFailure(
|
||||
BrowseFailureKind.ConnectionNotFound,
|
||||
$"No data connection named '{command.ConnectionName}' at this site.")));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Routes a <see cref="ReadTagValuesCommand"/> from the CentralUI's Test
|
||||
/// Bindings dialog to the child <see cref="DataConnectionActor"/> that
|
||||
|
||||
Reference in New Issue
Block a user