Extend HistorianTagMetadata with Description, EngineeringUnit, MinEU/MaxEU
Decoded the GetTagInfoFromName response shape across multiple tag types via captured raw bytes (sanitized decoder script in scripts/decode-taginfo-bytes.py): - Compact-ASCII string slot count varies by tag origin: 2 strings for MDAS-routed external tags (TagName + MetadataProvider), 4 strings for local Sys tags (TagName + Description + ItemName + CreatedBy). Parser now walks strings dynamically until the next byte isn't the 0x09 marker. - Trailing region after the 4-byte fixed block holds (for analog tags) two doubles for MinEU/MaxEU plus an optional EngineeringUnit compact ASCII string and other fields whose exact positions vary. Parser uses a tolerant scan: tries each 8-byte alignment 0..7, picks the first sane (Min ≤ Max, finite, not all-zeros, |x| ≤ 1e15) double pair as MinEU/MaxEU, and finds the first plausible compact ASCII string (1..32 ASCII bytes, not numeric) as EngineeringUnit. HistorianTagMetadata.Description / EngineeringUnit / MinRaw / MaxRaw nullable slots already existed; they're now populated. Live verification: SysTimeSec returns Description="System Time : Seconds", MaxRaw=59.0, EngineeringUnit ="Seconds". Tests: 109 → 114 (+4 synthetic-fixture parser tests + 1 live integration test for the populated analog metadata path). Bulk descriptor probe helper (GetTagInfoRawBytesForProbe) added for future layout work; raw bytes never committed because they contain CreatedBy DOMAIN\user identity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -300,4 +300,34 @@ public sealed class HistorianClientIntegrationTests
|
||||
Assert.Equal(testTag, metadata.Name);
|
||||
Assert.NotNull(metadata.Key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTagMetadataAsync_PopulatesDescriptionAndEuRangeForAnalogTag()
|
||||
{
|
||||
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
||||
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// SysTimeSec is a built-in analog UInt16 tag with non-empty Description, MaxEU,
|
||||
// and an EngineeringUnit. Verifies the parser populates those new fields end-to-end.
|
||||
const string analogTag = "SysTimeSec";
|
||||
HistorianClient client = new(new HistorianClientOptions
|
||||
{
|
||||
Host = host,
|
||||
IntegratedSecurity = true,
|
||||
Transport = HistorianTransport.LocalPipe
|
||||
});
|
||||
|
||||
AVEVA.Historian.Client.Models.HistorianTagMetadata? metadata =
|
||||
await client.GetTagMetadataAsync(analogTag, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(metadata);
|
||||
Assert.Equal(analogTag, metadata.Name);
|
||||
Assert.False(string.IsNullOrWhiteSpace(metadata.Description));
|
||||
Assert.NotNull(metadata.MaxRaw);
|
||||
Assert.True(metadata.MaxRaw is > 0 and <= 1e15);
|
||||
Assert.False(string.IsNullOrWhiteSpace(metadata.EngineeringUnit));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user