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:
Joseph Doherty
2026-03-17 18:25:39 -04:00
parent adc1af9f16
commit 775cb8084f
6 changed files with 76 additions and 6 deletions

View File

@@ -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`) |
|---|---|---|

View File

@@ -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.

View File

@@ -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**.

View File

@@ -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);

View File

@@ -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(

View File

@@ -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);
}
}