Files
histsdk/scripts/decode-write-capture.py
T
dohertj2 b5f9a71fe7 write-commands plan: Phase 2 partial - capture EnsT2(Float) wire bytes
Per plan §1 in scope: EnsT2 for analog tags, AddS2, DelT.
Per plan §2 safety: localhost only, single sandbox tag
RetestSdkWriteSandbox, harness refuses any name not starting with
RetestSdkWrite, time-bounded writes, ReadOnly=false only when scenario
is "write".

Phase 2 actually executed:

1. tools/AVEVA.Historian.NativeTraceHarness/Program.cs extended with
   --scenario write. New args:
     --write-sandbox-tag <name>  (default RetestSdkWriteSandbox)
     --write-value <numeric>     (default 42.5)
     --write-data-type <name>    (default Float)
     --write-delete-after        (best-effort cleanup)
   Toggles ConnectionArgs.ReadOnly=false when scenario is "write" so
   the connection accepts the write attempt instead of rejecting at
   the harness boundary with error 132 "Operation is not enabled".

2. Sandbox tag RetestSdkWriteSandbox created in Runtime DB
   (wwTagKey=240, AcquisitionType=2 Manual, StorageType=1 Cyclic)
   via the harness's AddTag call. Single dedicated tag per safety §1.

3. Captured the full write-flow wire sequence at
   artifacts/reverse-engineering/instrumented-wcf-writemessage-writes/
   bothmessage-write-capture-latest.ndjson (46 records, 23 outgoing +
   23 incoming).

   The chain is identical to the event flow except:
     - EnsT2 payload is the 146-byte analog CTagMetadata instead of
       the 83-byte event one
     - NO RTag2 between Open2 and EnsT2 (events used RTag2 with
       CmEventTagId)

4. The 146-byte analog CTagMetadata layout is dumped in the plan doc
   for layout decoding. Visible fields (still being aligned against
   CTagUtil.ConvertTagMetadataToHistorianTag IL at token 0x060055CE):
     - tag name "RetestSdkWriteSandbox" (compact ASCII, len 21)
     - 16 bytes of FF (CommonArchestraEventTypeId placeholder unused
       for analog?)
     - description "SDK write-RE sandbox tag" (compact ASCII, len 24)
     - metadata provider "MDAS" (compact ASCII)
     - engineering unit "test" (compact ASCII)
     - Int64 FILETIME (date-created, year 2026)
     - uint32 0x2710 = 10000 (storage-related, possibly StorageRate)
     - double 1.0 (likely IntegralDivisor or scaling factor)
     - 5-byte trailer FE 00 01 01 01 (matches event tag's
       2F 27 01 01 01 shape)

5. AddS2 BLOCKED CLIENT-SIDE at error 168 "Tag not added to server".
   Native AddStreamedValue refuses to send because the tag isn't in
   the server's session cache, even though EnsT2 created it in the
   Runtime DB. Likely needs RTag2(analog tag GUID) prereq similar
   to the event flow's RTag2(CmEventTagId), or one of
   aahClientCommon.CHistStorage.AddTagidPairs (token 0x0600202F) or
   AddTagsWithServerTagId (token 0x06002026). AddS2 wire bytes NOT
   captured this session.

6. scripts/decode-write-capture.py — sanitized decoder for the
   capture, walks the 46 records and dumps the EnsT2 InBuff bytes
   for layout work. No identity strings; only sandbox-chosen values
   appear in output.

Phase 2 remaining work documented in the plan doc as a 5-item
checklist for the next session:
  1. Decode the AddS2 prereq (likely RTag2 with analog tag GUID).
  2. Capture AddS2 wire bytes once prereq is satisfied.
  3. Implement HistorianAddTagsProtocol.SerializeAnalog/Discrete/
     String CTagMetadata variants.
  4. Implement HistorianAddStreamValuesProtocol.Serialize.
  5. Implement public surface: EnsureTagAsync, WriteValueAsync,
     DeleteTagAsync (golden-byte + gated live integration tests).

No SDK source changed — implementation deferred until AddS2 wire
bytes are in hand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:55:27 -04:00

80 lines
2.8 KiB
Python

"""Decoder for the write-flow capture at
artifacts/reverse-engineering/instrumented-wcf-writemessage-writes/
bothmessage-write-capture-latest.ndjson.
Lists the 46-record write-flow inventory and prints the EnsT2(analog)
CTagMetadata payload byte-by-byte for layout decoding. Output stays
commit-safe — the only string values printed are the sandbox tag name,
description, and engineering unit, all of which were chosen for the
sandbox itself (not customer data).
"""
import base64
import json
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
CAPTURE = REPO_ROOT / "artifacts" / "reverse-engineering" / "instrumented-wcf-writemessage-writes" / "bothmessage-write-capture-latest.ndjson"
ACTION_RE = re.compile(rb"aa/(?:Hist|Retr|Trx|Stat|Stor)/[A-Za-z0-9]+(?:Response)?")
def main() -> int:
if not CAPTURE.exists():
print(f"Capture file not found: {CAPTURE}")
return 1
with CAPTURE.open(encoding="utf-8-sig") as fh:
records = [json.loads(line) for line in fh if line.strip()]
print(f"Total records: {len(records)}")
print()
print(f"{'#':>3} {'Phase':<6} {'Length':>6} {'Action':<48}")
print("-" * 70)
for idx, rec in enumerate(records):
body = base64.b64decode(rec["Base64"])
match = ACTION_RE.search(body)
action = match.group(0).decode() if match else "<no-action>"
phase = "Write" if rec["Phase"] == "WCF.WriteMessage.Body" else "Read"
print(f"{idx:>3} {phase:<6} {rec['Length']:>6} {action:<48}")
# Find the EnsT2 outgoing record and dump its InBuff payload.
print()
print("== EnsT2(Float) CTagMetadata payload ==")
for idx, rec in enumerate(records):
if rec["Phase"] != "WCF.WriteMessage.Body":
continue
body = base64.b64decode(rec["Base64"])
if b"aa/Hist/EnsT2" not in body:
continue
i = body.find(b"InBuff")
marker = body[i + 6]
if marker == 0x9F:
length = int.from_bytes(body[i + 7:i + 9], "little")
payload = body[i + 9:i + 9 + length]
elif marker == 0x9E:
length = body[i + 7]
payload = body[i + 8:i + 8 + length]
elif marker == 0xA0:
length = int.from_bytes(body[i + 7:i + 9], "little")
payload = body[i + 9:i + 9 + length + 1]
else:
print(f" Unknown marker 0x{marker:02X}; cannot extract")
return 2
print(f" Record {idx}: {len(payload)} bytes")
for off in range(0, len(payload), 16):
chunk = payload[off:off + 16]
hp = " ".join(f"{c:02X}" for c in chunk)
ap = "".join(chr(c) if 32 <= c < 127 else "." for c in chunk)
print(f" {off:04X} {hp:<48} |{ap}|")
return 0
print(" (No EnsT2 record found.)")
return 0
if __name__ == "__main__":
sys.exit(main())