5310952ab2
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>
106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
"""Decode a hex-encoded GetTagInfoFromName response into a sanitized field map.
|
|
|
|
USE: pipe one tag's hex on stdin (no spaces), one per line. Identity-bearing string
|
|
values are reported only as length + non-ASCII flags so the output stays commit-safe.
|
|
"""
|
|
import struct
|
|
import sys
|
|
import binascii
|
|
|
|
|
|
def is_printable_ascii(b: bytes) -> bool:
|
|
return all(0x20 <= c < 0x7F for c in b)
|
|
|
|
|
|
def decode_one(hex_str: str) -> None:
|
|
raw = binascii.unhexlify(hex_str)
|
|
n = len(raw)
|
|
cur = 0
|
|
|
|
# Fixed prefix
|
|
desc = raw[cur:cur+4]; cur += 4
|
|
type_id = raw[cur:cur+16]; cur += 16
|
|
tag_key = struct.unpack_from('<I', raw, cur)[0]; cur += 4
|
|
|
|
print(f" total_bytes = {n}")
|
|
print(f" [00..03] descriptor = {desc.hex().upper()}")
|
|
print(f" [04..19] typeId(GUID) = (16 bytes)")
|
|
print(f" [20..23] tagKey(uint32)= {tag_key}")
|
|
|
|
# Walk compact ASCII strings until we hit a byte != 0x09
|
|
string_idx = 1
|
|
while cur < n and raw[cur] == 0x09:
|
|
marker_off = cur
|
|
cur += 1
|
|
length = struct.unpack_from('<H', raw, cur)[0]; cur += 2
|
|
value = raw[cur:cur + length]
|
|
cur += length
|
|
is_ascii = is_printable_ascii(value)
|
|
print(f" [{marker_off:02X}..{cur-1:02X}] string{string_idx} = "
|
|
f"compactAscii(len={length}, ascii={is_ascii})")
|
|
string_idx += 1
|
|
|
|
# 4-byte fixed block (nativeTagClass, storageType, deadbandType, interpolationType)
|
|
if cur + 4 <= n:
|
|
block = raw[cur:cur+4]
|
|
print(f" [{cur:02X}..{cur+3:02X}] fixedBlock4 = "
|
|
f"class={block[0]} storage={block[1]} deadband={block[2]} interp={block[3]}")
|
|
cur += 4
|
|
|
|
# Try to identify trailing fields by structure
|
|
print(f" trailing bytes from offset 0x{cur:02X} ({n - cur} bytes):")
|
|
trailing = raw[cur:]
|
|
# Look for FILETIME pattern (year-2025 = 0x01DCxxxxxxxxxxxx, byte 7 = 0x01)
|
|
for off in range(min(len(trailing) - 7, 16)):
|
|
if trailing[off + 7] == 0x01 and trailing[off + 6] in (0xDB, 0xDC, 0xDD):
|
|
ft_bytes = trailing[off:off+8]
|
|
ft_value = struct.unpack_from('<q', ft_bytes)[0]
|
|
# FILETIME → year approximation
|
|
seconds = ft_value / 10_000_000
|
|
years_since_1601 = seconds / 31_557_600
|
|
year = 1601 + int(years_since_1601)
|
|
print(f" candidate FILETIME at trailing+0x{off:X}: year~{year} "
|
|
f"(absolute offset 0x{cur + off:02X})")
|
|
|
|
# Look for double 10.0 (0x4024000000000000) and other doubles
|
|
for off in range(0, len(trailing) - 7, 1):
|
|
try:
|
|
d = struct.unpack_from('<d', trailing, off)[0]
|
|
if d != 0 and abs(d) < 1e10 and abs(d) > 1e-5:
|
|
print(f" double @trailing+0x{off:X} (abs 0x{cur+off:02X}) = {d}")
|
|
except Exception:
|
|
pass
|
|
|
|
# Look for plausible uint32s (small, non-zero)
|
|
for off in range(0, len(trailing) - 3, 1):
|
|
v = struct.unpack_from('<I', trailing, off)[0]
|
|
if 0 < v < 1_000_000:
|
|
print(f" uint32 @trailing+0x{off:X} (abs 0x{cur+off:02X}) = {v}")
|
|
|
|
# Look for trailing marker (FE 00 or 00)
|
|
if len(trailing) >= 2:
|
|
last2 = trailing[-2:]
|
|
last1 = trailing[-1:]
|
|
print(f" last 2 bytes: {last2.hex().upper()}")
|
|
|
|
print()
|
|
|
|
|
|
def main() -> int:
|
|
for line in sys.stdin:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
# Allow optional "TagName: HEX" prefix
|
|
if ':' in line:
|
|
label, hex_str = line.split(':', 1)
|
|
print(f"=== {label.strip()} ===")
|
|
decode_one(hex_str.strip())
|
|
else:
|
|
decode_one(line)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|