fix(site-runtime): fan tag updates out to every attribute sharing a tag path
InstanceActor._tagPathToAttribute was a Dictionary<string,string> — one tag path mapped to a single attribute. When two attributes reference the same PLC node (e.g. two composed cooling-tank modules both reading ns=3;s=Tank.Level, or a pump's TempSensor and AlarmSensor both reading ns=3;s=Sensor.Reading), SubscribeToDcl's map assignment overwrote, so only the last-registered attribute ever received values — the rest stayed permanently Uncertain. The map is now Dictionary<string,List<string>>; HandleTagValueUpdate fans each update out to every attribute referencing the tag path, and each distinct tag path is still subscribed only once per connection.
This commit is contained in:
@@ -2,6 +2,8 @@ using Akka.Actor;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ScadaLink.Commons.Interfaces.Protocol;
|
||||
using ScadaLink.Commons.Messages.DataConnection;
|
||||
using ScadaLink.Commons.Messages.Instance;
|
||||
using ScadaLink.Commons.Messages.Lifecycle;
|
||||
using ScadaLink.Commons.Types.Flattening;
|
||||
@@ -306,4 +308,63 @@ public class InstanceActorTests : TestKit, IDisposable
|
||||
Assert.True(response.Found);
|
||||
Assert.Equal("Good", response.Quality);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single PLC tag can back more than one attribute — e.g. two composed
|
||||
/// cooling-tank modules whose members both reference the one simulated
|
||||
/// <c>ns=3;s=Tank.Level</c> node. A <see cref="TagValueUpdate"/> must fan out
|
||||
/// to every attribute that references that tag path, not just the last one
|
||||
/// registered: the tag-path → attribute map previously overwrote on a shared
|
||||
/// tag, leaving all but one attribute permanently Uncertain.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InstanceActor_TagUpdate_FansOutToEveryAttributeSharingTheTagPath()
|
||||
{
|
||||
const string sharedTag = "ns=3;s=Tank.Level";
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Motor-1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute
|
||||
{
|
||||
CanonicalName = "CoolingTank.Level", Value = "0", DataType = "Int",
|
||||
DataSourceReference = sharedTag, BoundDataConnectionName = "PLC"
|
||||
},
|
||||
new ResolvedAttribute
|
||||
{
|
||||
CanonicalName = "CoolingTank2.Level", Value = "0", DataType = "Int",
|
||||
DataSourceReference = sharedTag, BoundDataConnectionName = "PLC"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var dcl = CreateTestProbe();
|
||||
var actor = ActorOf(Props.Create(() => new InstanceActor(
|
||||
"Motor-1",
|
||||
JsonSerializer.Serialize(config),
|
||||
_storage,
|
||||
_compilationService,
|
||||
_sharedScriptLibrary,
|
||||
null,
|
||||
_options,
|
||||
NullLogger<InstanceActor>.Instance,
|
||||
dcl.Ref)));
|
||||
|
||||
// On startup the actor subscribes its data-sourced tags through the DCL.
|
||||
dcl.ExpectMsg<SubscribeTagsRequest>(TimeSpan.FromSeconds(5));
|
||||
|
||||
// One value arrives for the tag that both attributes reference.
|
||||
actor.Tell(new TagValueUpdate("PLC", sharedTag, 47, QualityCode.Good, DateTimeOffset.UtcNow));
|
||||
|
||||
// BOTH attributes must reflect it — not just the last-registered one.
|
||||
foreach (var attrName in new[] { "CoolingTank.Level", "CoolingTank2.Level" })
|
||||
{
|
||||
actor.Tell(new GetAttributeRequest("corr-fanout", "Motor-1", attrName, DateTimeOffset.UtcNow));
|
||||
var response = ExpectMsg<GetAttributeResponse>(TimeSpan.FromSeconds(5));
|
||||
Assert.True(response.Found);
|
||||
Assert.Equal("47", response.Value?.ToString());
|
||||
Assert.Equal("Good", response.Quality);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user