Wire DCL to Instance Actors for OPC UA tag value flow

- Add TagValueUpdate/ConnectionQualityChanged handlers to InstanceActor
- InstanceActor subscribes to DCL on PreStart based on DataSourceReference
- DeploymentManagerActor creates DCL connections on deploy and passes DCL ref
- AkkaHostedService creates DCL Manager Actor for tag subscriptions
- Move CreateConnectionCommand to Commons for cross-project access
- Add ConnectionConfig to FlattenedConfiguration for deployment packaging
This commit is contained in:
Joseph Doherty
2026-03-17 11:21:11 -04:00
parent 2798b91fe1
commit dfb809a909
6 changed files with 175 additions and 12 deletions

View File

@@ -1,5 +1,6 @@
using Akka.Actor;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Messages.DataConnection;
using ScadaLink.Commons.Messages.DebugView;
using ScadaLink.Commons.Messages.Instance;
using ScadaLink.Commons.Messages.Lifecycle;
@@ -45,6 +46,11 @@ public class InstanceActor : ReceiveActor
// WP-25: Debug view subscribers
private readonly Dictionary<string, IActorRef> _debugSubscribers = new();
// DCL manager actor reference for subscribing to tag values
private readonly IActorRef? _dclManager;
// Maps tag paths back to attribute canonical names for DCL updates
private readonly Dictionary<string, string> _tagPathToAttribute = new();
public InstanceActor(
string instanceUniqueName,
string configJson,
@@ -53,7 +59,8 @@ public class InstanceActor : ReceiveActor
SharedScriptLibrary sharedScriptLibrary,
SiteStreamManager? streamManager,
SiteRuntimeOptions options,
ILogger logger)
ILogger logger,
IActorRef? dclManager = null)
{
_instanceUniqueName = instanceUniqueName;
_storage = storage;
@@ -62,6 +69,7 @@ public class InstanceActor : ReceiveActor
_streamManager = streamManager;
_options = options;
_logger = logger;
_dclManager = dclManager;
// Deserialize the flattened configuration
_configuration = JsonSerializer.Deserialize<FlattenedConfiguration>(configJson);
@@ -102,6 +110,10 @@ public class InstanceActor : ReceiveActor
// WP-22/23: Handle attribute value changes from DCL (Tell pattern)
Receive<AttributeValueChanged>(HandleAttributeValueChanged);
// Handle tag value updates from DCL — convert to AttributeValueChanged
Receive<TagValueUpdate>(HandleTagValueUpdate);
Receive<ConnectionQualityChanged>(HandleConnectionQualityChanged);
// WP-16: Handle alarm state changes from Alarm Actors (Tell pattern)
Receive<AlarmStateChanged>(HandleAlarmStateChanged);
@@ -129,6 +141,9 @@ public class InstanceActor : ReceiveActor
// Create child Script Actors and Alarm Actors from configuration
CreateChildActors();
// Subscribe to DCL for data-sourced attributes
SubscribeToDcl();
}
/// <summary>
@@ -238,6 +253,66 @@ public class InstanceActor : ReceiveActor
PublishAndNotifyChildren(changed);
}
/// <summary>
/// Handles tag value updates from DCL. Maps the tag path back to the attribute
/// canonical name and converts to an AttributeValueChanged for unified processing.
/// </summary>
private void HandleTagValueUpdate(TagValueUpdate update)
{
if (_tagPathToAttribute.TryGetValue(update.TagPath, out var attrName))
{
var changed = new AttributeValueChanged(
_instanceUniqueName, update.TagPath, attrName,
update.Value, update.Quality.ToString(), update.Timestamp);
HandleAttributeValueChanged(changed);
}
}
private void HandleConnectionQualityChanged(ConnectionQualityChanged qualityChanged)
{
_logger.LogInformation("Connection {Connection} quality changed to {Quality}",
qualityChanged.ConnectionName, qualityChanged.Quality);
}
/// <summary>
/// Subscribes to DCL for all data-sourced attributes. Groups tag paths by connection
/// name and sends SubscribeTagsRequest to the DCL manager.
/// </summary>
private void SubscribeToDcl()
{
if (_dclManager == null || _configuration == null) return;
// Group attributes by their bound connection name
var byConnection = new Dictionary<string, List<string>>();
foreach (var attr in _configuration.Attributes)
{
if (string.IsNullOrEmpty(attr.DataSourceReference) ||
string.IsNullOrEmpty(attr.BoundDataConnectionName))
continue;
_tagPathToAttribute[attr.DataSourceReference] = attr.CanonicalName;
if (!byConnection.ContainsKey(attr.BoundDataConnectionName))
byConnection[attr.BoundDataConnectionName] = new List<string>();
byConnection[attr.BoundDataConnectionName].Add(attr.DataSourceReference);
}
// Send subscription requests to DCL for each connection
foreach (var (connectionName, tagPaths) in byConnection)
{
var request = new SubscribeTagsRequest(
Guid.NewGuid().ToString("N"),
_instanceUniqueName,
connectionName,
tagPaths,
DateTimeOffset.UtcNow);
_dclManager.Tell(request, Self);
_logger.LogInformation(
"Instance {Instance} subscribed to {Count} tags on connection {Connection}",
_instanceUniqueName, tagPaths.Count, connectionName);
}
}
/// <summary>
/// WP-16: Handles alarm state changes from Alarm Actors.
/// Updates in-memory alarm state and publishes to stream.