Files
histsdk/docs/reverse-engineering/wcf-tag-extended-properties.md
T
Joseph Doherty 108220c36b R1.5 GetTagExtendedPropertiesAsync (GetTepByNm) + R1.6 closed (no op)
Ship tag extended-property reads over the 2020 WCF aa/Retr/GetTepByNm op:
HistorianClient.GetTagExtendedPropertiesAsync(tag) -> name/value pairs.

String-handle op reached with the Open2 storage-session GUID formatted
uppercase (same format that unlocked GETRP/GETHI/ExeC). Routed via the
name-based native path (GetTagExtendedPropertiesByName, server-fetch flag),
not the index-based TagQuery path.

Evidence-backed findings from the capture:
- GetTepByNm (and GetTgByNm) succeed with the uppercase handle -- further
  validates the resolved string-handle wall.
- QTB (StartTagQuery) does NOT punch through: captured uppercase, it still
  fails server-side (CMdServer::StartActiveTagnamesQuery over the
  aahMetadataServer pipe) -- a metadata-server blocker, not handle format.
- R1.6 (localized properties) has NO distinct op (only error-message/UI-text
  localization in the managed client); collapses into R1.5. Closed, not throwing.

Wire format (golden-pinned, synthetic bytes -- no dev tag names committed):
- request tagNames = uint count + per-name(uint charCount + UTF-16)
- response = uint tagCount + per-tag(marker + compact-ASCII name +
  uint propCount + per-prop(marker + compact-ASCII name + 0x43 VT_BSTR value)
  + trailer); sequence-paged.

Adds: HistorianTagExtendedProperty model, HistorianTagExtendedPropertyProtocol
(codec), HistorianWcfTagExtendedPropertyClient (orchestration), dialect +
public API; golden WcfTagExtendedPropertyProtocolTests (4) + gated live test
(HISTORIAN_TEP_TAG). Tooling: Capture-TagExtendedProperties.ps1,
decode-tag-properties-capture.py, harness tag-extended-properties scenario.
Docs: wcf-tag-extended-properties.md; roadmap R1.5 DONE / R1.6 collapsed;
wall doc + memory updated with the QTB-server-side nuance. 228 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
2026-06-20 22:52:07 -04:00

4.9 KiB

Tag extended properties over 2020 WCF — GetTepByNm (HCAL R1.5)

Status: DONE + live-verified (2026-06-20). HistorianClient.GetTagExtendedPropertiesAsync(tag) reads a tag's extended (user-defined) properties over the 2020 WCF op aa/Retr/GetTagExtendedPropertiesFromName (GetTepByNm). Live-verified end-to-end from the pure-managed .NET 10 client against the local 2020 Historian.

The op

GetTepByNm is on the Retrieval service (IRetrievalServiceContract4):

bool GetTagExtendedPropertiesFromName(
    string handle,                       // Open2 storage-session GUID, UPPERCASE dash-no-braces
    byte[]  tagNames,                    // [MessageParameter pRequestBuff-style]
    ref uint sequence,                   // paging cursor (0 on first call)
    out byte[] tagExtendedProperties,    // result buffer
    out byte[] errorBuffer)

It is a string-handle op — reachable from the managed client because the handle is the Open2 storage-session GUID formatted storageSessionId.ToString("D").ToUpperInvariant() (the same handle format that unlocked GETRP/GETHI/ExeC; see wcf-string-handle-wall.md). The Retrieval service version handshake (Retr.GetV) is primed first, as the native client does.

Why the name-based path (not the TagQuery path)

There are two managed entry points:

  • Index-based TagQuery.GetTagExtendedPropertyInfo(start, count, …) — requires a prior StartTagQuery (QTB). On this 2020 box QTB fails server-side (error 1 from CMdServer::StartTagQuery::StartActiveTagnamesQuery over \\.\pipe\aahMetadataServer\console), so this path is dead here regardless of handle format.
  • Name-based HistorianAccess.GetTagExtendedPropertiesByName(tagName, fetchFromServer, …) — issues GetTepByNm directly with the tag name in tagNames, no QTB needed. Its second arg forces a server fetch when true; when false the C++ client reads a local cache and returns error 41 (Requested item not found) without any WCF round-trip. The SDK reproduces the name-based path.

Wire format (captured)

scripts/Capture-TagExtendedProperties.ps1 (NativeTraceHarness tag-extended-properties scenario + instrument-wcf-{write,read}message) → decode with scripts/decode-tag-properties-capture.py. Golden-pinned in WcfTagExtendedPropertyProtocolTests.

Request — tagNames buffer

uint32 count
per name: uint32 charCount + UTF-16LE chars

(For one tag: 01 00 00 00 + LL 00 00 00 + UTF-16 name.)

Response — tagExtendedProperties buffer

uint32 tagCount
per tag:
    byte   groupMarker            (observed 0x01)
    0x09 + uint16 byteLen + ASCII tagName        (compact-ASCII string)
    uint32 propertyCount
    per property:
        byte   propMarker         (observed 0x02 — likely the value type)
        0x09 + uint16 byteLen + ASCII propertyName
        0x43 + uint16 payloadLen + uint16 charCount + UTF-16LE value   (VT_BSTR CRetVariant)
    byte   trailingMarker         (observed 0x01)

payloadLen counts the charCount field (2 bytes) + the UTF-16 value bytes. Only the string value variant (0x43) is evidence-backed; other variant types throw ProtocolEvidenceMissingException (same discipline as GETRP). The single-byte 0x01/0x02/0x01 markers are pinned as observed constants from a single capture; their full semantics are not independently disambiguated.

Example (sanitized — real capture used a dev tag/value):

tag "Reactor.Temp1" → property "Location" = "Plant/AreaA"

Paging

GetTepByNm is sequence-paged like GetNextQueryResultBuffer: call with sequence = 0, parse the buffer, then re-call with the returned sequence. A small result returns everything on the first call; the next call returns an empty/nil buffer (with a benign CClientUtil::FillBufferFromVector terminator) — that is the stop signal. The SDK loops until the buffer carries no rows.

R1.6 (localized properties) — no distinct op on 2020

There is no GetTagLocalizedPropertiesFromName / GetTlpByNm op or GetTagLocalizedPropertiesByName method in current/aahClientManaged.dll — the only "localized" surfaces are ClientApp.GetLocalizedText and SMessageTextMap.GetLocalizedMessage (error-message / UI-text localization), not tag properties. So R1.6 collapses into R1.5: extended properties (GetTepByNm) are the user-defined tag-property read surface on 2020. R1.6 is closed as "no separate op," not left throwing.

Shipped surface

  • HistorianClient.GetTagExtendedPropertiesAsync(tag)IReadOnlyList<HistorianTagExtendedProperty> (Name/Value pairs; empty when the tag has none).
  • HistorianTagExtendedPropertyProtocol (serializer/parser), HistorianWcfTagExtendedPropertyClient (orchestration), golden WcfTagExtendedPropertyProtocolTests, gated live GetTagExtendedPropertiesAsync_AgainstLocalHistorian_ReturnsProperties (set HISTORIAN_TEP_TAG to a tag with extended properties).