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
|
### 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`) |
|
| Concept | ScadaLink Design | LmxProxy SDK (`Vtq`) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|||||||
@@ -87,9 +87,10 @@ Deployment Manager Singleton (Cluster Singleton)
|
|||||||
|
|
||||||
### Initialization
|
### Initialization
|
||||||
1. Load all attribute values from the flattened configuration (static defaults).
|
1. Load all attribute values from the flattened configuration (static defaults).
|
||||||
2. Register data source references with the Data Connection Layer for subscriptions.
|
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. Create child Script Actors (one per script defined on the instance).
|
3. Register data source references with the Data Connection Layer for subscriptions.
|
||||||
4. Create child Alarm Actors (one per alarm defined on the instance).
|
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
|
### Attribute Value Updates
|
||||||
- Receives tag value updates from the Data Connection Layer for attributes with data source references.
|
- 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.
|
- 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.
|
- 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.
|
- 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
|
### 2.5 Scale
|
||||||
- Approximately **10 sites**.
|
- Approximately **10 sites**.
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ public record GetAttributeRequest(
|
|||||||
DateTimeOffset Timestamp);
|
DateTimeOffset Timestamp);
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public record GetAttributeResponse(
|
public record GetAttributeResponse(
|
||||||
string CorrelationId,
|
string CorrelationId,
|
||||||
@@ -19,4 +21,5 @@ public record GetAttributeResponse(
|
|||||||
string AttributeName,
|
string AttributeName,
|
||||||
object? Value,
|
object? Value,
|
||||||
bool Found,
|
bool Found,
|
||||||
|
string Quality,
|
||||||
DateTimeOffset Timestamp);
|
DateTimeOffset Timestamp);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ public class InstanceActor : ReceiveActor
|
|||||||
private readonly SiteRuntimeOptions _options;
|
private readonly SiteRuntimeOptions _options;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly Dictionary<string, object?> _attributes = new();
|
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, AlarmState> _alarmStates = new();
|
||||||
private readonly Dictionary<string, IActorRef> _scriptActors = new();
|
private readonly Dictionary<string, IActorRef> _scriptActors = new();
|
||||||
private readonly Dictionary<string, IActorRef> _alarmActors = new();
|
private readonly Dictionary<string, IActorRef> _alarmActors = new();
|
||||||
@@ -75,11 +76,15 @@ public class InstanceActor : ReceiveActor
|
|||||||
_configuration = JsonSerializer.Deserialize<FlattenedConfiguration>(configJson);
|
_configuration = JsonSerializer.Deserialize<FlattenedConfiguration>(configJson);
|
||||||
|
|
||||||
// Load default attribute values from the flattened configuration
|
// 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)
|
if (_configuration != null)
|
||||||
{
|
{
|
||||||
foreach (var attr in _configuration.Attributes)
|
foreach (var attr in _configuration.Attributes)
|
||||||
{
|
{
|
||||||
_attributes[attr.CanonicalName] = attr.Value;
|
_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)
|
private void HandleGetAttribute(GetAttributeRequest request)
|
||||||
{
|
{
|
||||||
var found = _attributes.TryGetValue(request.AttributeName, out var value);
|
var found = _attributes.TryGetValue(request.AttributeName, out var value);
|
||||||
|
_attributeQualities.TryGetValue(request.AttributeName, out var quality);
|
||||||
Sender.Tell(new GetAttributeResponse(
|
Sender.Tell(new GetAttributeResponse(
|
||||||
request.CorrelationId,
|
request.CorrelationId,
|
||||||
_instanceUniqueName,
|
_instanceUniqueName,
|
||||||
request.AttributeName,
|
request.AttributeName,
|
||||||
value,
|
value,
|
||||||
found,
|
found,
|
||||||
|
quality ?? "Good",
|
||||||
DateTimeOffset.UtcNow));
|
DateTimeOffset.UtcNow));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,6 +256,7 @@ public class InstanceActor : ReceiveActor
|
|||||||
{
|
{
|
||||||
// WP-24: State mutation serialized through this actor
|
// WP-24: State mutation serialized through this actor
|
||||||
_attributes[changed.AttributeName] = changed.Value;
|
_attributes[changed.AttributeName] = changed.Value;
|
||||||
|
_attributeQualities[changed.AttributeName] = changed.Quality;
|
||||||
|
|
||||||
PublishAndNotifyChildren(changed);
|
PublishAndNotifyChildren(changed);
|
||||||
}
|
}
|
||||||
@@ -345,7 +353,7 @@ public class InstanceActor : ReceiveActor
|
|||||||
kvp.Key,
|
kvp.Key,
|
||||||
kvp.Key,
|
kvp.Key,
|
||||||
kvp.Value,
|
kvp.Value,
|
||||||
"Good",
|
_attributeQualities.GetValueOrDefault(kvp.Key, "Good"),
|
||||||
DateTimeOffset.UtcNow)).ToList();
|
DateTimeOffset.UtcNow)).ToList();
|
||||||
|
|
||||||
var alarmStates = _alarmStates.Select(kvp => new AlarmStateChanged(
|
var alarmStates = _alarmStates.Select(kvp => new AlarmStateChanged(
|
||||||
|
|||||||
@@ -222,4 +222,61 @@ public class InstanceActorTests : TestKit, IDisposable
|
|||||||
var response = ExpectMsg<GetAttributeResponse>();
|
var response = ExpectMsg<GetAttributeResponse>();
|
||||||
Assert.Equal("98.6", response.Value?.ToString());
|
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