feat: data-sourced attributes start with uncertain quality before first DCL value
Attributes bound to data connections now initialize with "Uncertain" quality, distinguishing "never received a value" from "known good" or "connection lost." Quality is tracked per attribute and included in GetAttributeResponse.
This commit is contained in:
@@ -54,7 +54,7 @@ Additional protocols can be added by implementing this interface.
|
||||
|
||||
### Common Value Type
|
||||
|
||||
Both protocols produce the same value tuple consumed by Instance Actors:
|
||||
Both protocols produce the same value tuple consumed by Instance Actors. Before the first value update arrives from the DCL, data-sourced attributes are held at **uncertain** quality by the Instance Actor (see Site Runtime — Initialization):
|
||||
|
||||
| Concept | ScadaLink Design | LmxProxy SDK (`Vtq`) |
|
||||
|---|---|---|
|
||||
|
||||
@@ -87,9 +87,10 @@ Deployment Manager Singleton (Cluster Singleton)
|
||||
|
||||
### Initialization
|
||||
1. Load all attribute values from the flattened configuration (static defaults).
|
||||
2. Register data source references with the Data Connection Layer for subscriptions.
|
||||
3. Create child Script Actors (one per script defined on the instance).
|
||||
4. Create child Alarm Actors (one per alarm defined on the instance).
|
||||
2. Set quality to **uncertain** for all attributes that have a data source reference. Static attributes (no data source reference) have quality **good**. The uncertain quality persists until the first value update arrives from the Data Connection Layer, distinguishing "not yet received" from "known good" or "connection lost."
|
||||
3. Register data source references with the Data Connection Layer for subscriptions.
|
||||
4. Create child Script Actors (one per script defined on the instance).
|
||||
5. Create child Alarm Actors (one per alarm defined on the instance).
|
||||
|
||||
### Attribute Value Updates
|
||||
- Receives tag value updates from the Data Connection Layer for attributes with data source references.
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
- Both protocols implement a **common interface** supporting: connect, subscribe to tag paths, receive value updates, and write values.
|
||||
- Additional protocols can be added by implementing the common interface.
|
||||
- The Data Connection Layer is a **clean data pipe** — it publishes tag value updates to Instance Actors but performs no evaluation of triggers or alarm conditions.
|
||||
- **Initial attribute quality**: Attributes bound to a data connection start with **uncertain** quality when the Instance Actor initializes. The quality remains uncertain until the first value update is received from the Data Connection Layer. This distinguishes "never received a value" from "received a known-good value" or "connection lost" (bad quality).
|
||||
|
||||
### 2.5 Scale
|
||||
- Approximately **10 sites**.
|
||||
|
||||
@@ -11,7 +11,9 @@ public record GetAttributeRequest(
|
||||
DateTimeOffset Timestamp);
|
||||
|
||||
/// <summary>
|
||||
/// Response containing the current value of an attribute.
|
||||
/// Response containing the current value and quality of an attribute.
|
||||
/// Quality is "Good", "Bad", or "Uncertain".
|
||||
/// Data-sourced attributes start at "Uncertain" until the first DCL value update arrives.
|
||||
/// </summary>
|
||||
public record GetAttributeResponse(
|
||||
string CorrelationId,
|
||||
@@ -19,4 +21,5 @@ public record GetAttributeResponse(
|
||||
string AttributeName,
|
||||
object? Value,
|
||||
bool Found,
|
||||
string Quality,
|
||||
DateTimeOffset Timestamp);
|
||||
|
||||
@@ -38,6 +38,7 @@ public class InstanceActor : ReceiveActor
|
||||
private readonly SiteRuntimeOptions _options;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Dictionary<string, object?> _attributes = new();
|
||||
private readonly Dictionary<string, string> _attributeQualities = new();
|
||||
private readonly Dictionary<string, AlarmState> _alarmStates = new();
|
||||
private readonly Dictionary<string, IActorRef> _scriptActors = new();
|
||||
private readonly Dictionary<string, IActorRef> _alarmActors = new();
|
||||
@@ -75,11 +76,15 @@ public class InstanceActor : ReceiveActor
|
||||
_configuration = JsonSerializer.Deserialize<FlattenedConfiguration>(configJson);
|
||||
|
||||
// Load default attribute values from the flattened configuration
|
||||
// Data-sourced attributes start with Uncertain quality until the first DCL value arrives.
|
||||
// Static attributes start with Good quality.
|
||||
if (_configuration != null)
|
||||
{
|
||||
foreach (var attr in _configuration.Attributes)
|
||||
{
|
||||
_attributes[attr.CanonicalName] = attr.Value;
|
||||
_attributeQualities[attr.CanonicalName] =
|
||||
string.IsNullOrEmpty(attr.DataSourceReference) ? "Good" : "Uncertain";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,12 +174,14 @@ public class InstanceActor : ReceiveActor
|
||||
private void HandleGetAttribute(GetAttributeRequest request)
|
||||
{
|
||||
var found = _attributes.TryGetValue(request.AttributeName, out var value);
|
||||
_attributeQualities.TryGetValue(request.AttributeName, out var quality);
|
||||
Sender.Tell(new GetAttributeResponse(
|
||||
request.CorrelationId,
|
||||
_instanceUniqueName,
|
||||
request.AttributeName,
|
||||
value,
|
||||
found,
|
||||
quality ?? "Good",
|
||||
DateTimeOffset.UtcNow));
|
||||
}
|
||||
|
||||
@@ -249,6 +256,7 @@ public class InstanceActor : ReceiveActor
|
||||
{
|
||||
// WP-24: State mutation serialized through this actor
|
||||
_attributes[changed.AttributeName] = changed.Value;
|
||||
_attributeQualities[changed.AttributeName] = changed.Quality;
|
||||
|
||||
PublishAndNotifyChildren(changed);
|
||||
}
|
||||
@@ -345,7 +353,7 @@ public class InstanceActor : ReceiveActor
|
||||
kvp.Key,
|
||||
kvp.Key,
|
||||
kvp.Value,
|
||||
"Good",
|
||||
_attributeQualities.GetValueOrDefault(kvp.Key, "Good"),
|
||||
DateTimeOffset.UtcNow)).ToList();
|
||||
|
||||
var alarmStates = _alarmStates.Select(kvp => new AlarmStateChanged(
|
||||
|
||||
@@ -222,4 +222,61 @@ public class InstanceActorTests : TestKit, IDisposable
|
||||
var response = ExpectMsg<GetAttributeResponse>();
|
||||
Assert.Equal("98.6", response.Value?.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstanceActor_DataSourcedAttribute_StartsWithUncertainQuality()
|
||||
{
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Pump1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute
|
||||
{
|
||||
CanonicalName = "Temperature",
|
||||
Value = "0",
|
||||
DataType = "Double",
|
||||
DataSourceReference = "/Motor/Temperature",
|
||||
BoundDataConnectionName = "OpcServer1"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var actor = CreateInstanceActor("Pump1", config);
|
||||
|
||||
actor.Tell(new GetAttributeRequest(
|
||||
"corr-quality-1", "Pump1", "Temperature", DateTimeOffset.UtcNow));
|
||||
|
||||
var response = ExpectMsg<GetAttributeResponse>();
|
||||
Assert.True(response.Found);
|
||||
Assert.Equal("Uncertain", response.Quality);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstanceActor_StaticAttribute_StartsWithGoodQuality()
|
||||
{
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Pump1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute
|
||||
{
|
||||
CanonicalName = "Label",
|
||||
Value = "Main Pump",
|
||||
DataType = "String"
|
||||
// No DataSourceReference — static attribute
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var actor = CreateInstanceActor("Pump1", config);
|
||||
|
||||
actor.Tell(new GetAttributeRequest(
|
||||
"corr-quality-2", "Pump1", "Label", DateTimeOffset.UtcNow));
|
||||
|
||||
var response = ExpectMsg<GetAttributeResponse>();
|
||||
Assert.True(response.Found);
|
||||
Assert.Equal("Good", response.Quality);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user