f840af5873e549076b39f2eb82cbc3f82e2826cc
51 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
60b3673f01 |
M4 R4.4: client-side multi-historian redundancy
Adds AVEVA.Historian.Client.Redundancy — HistorianRedundantClient orchestrates N single-historian members (IHistorianMember; default HistorianClientMember over HistorianClient) as one logical client. Pure client-side, no server-side redundancy protocol, no RE. - Reads fail over to the next member in priority order. Streaming reads only fail over BEFORE the first row is observed; a mid-stream failure propagates (failing over mid-stream would risk duplicated/skipped rows). - Writes fan out: WriteFanout AllMembers | PreferredOnly, with All | Any ack policy, returning a per-member HistorianRedundantWriteResult. - Per-member health: FailureThreshold demotes a failing member out of the preferred pool; a background watchdog (PeriodicTimer) + CheckHealthAsync re-probe and restore recovered members. GetStatus() snapshot + ActiveMember. - Composes with R4.1: back a member's writes with a HistorianStoreForwardWriter so a down member buffers and replays on recovery — the pragmatic client-side equivalent of native ReSyncTags. 14 unit tests (no server): failover order, mid-stream no-failover, all-fail aggregation, probe-any-up, fan-out ack policies, PreferredOnly, soft reject, health demotion + CheckHealthAsync restore, watchdog recovery. Full suite 307 green. Roadmap R4.4 marked shipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
dd2aec3b8b |
M4 R4.1: pragmatic store-and-forward durable outbox
Adds AVEVA.Historian.Client.StoreForward — a client-side store-and-forward layer over the historian write surface (AddHistoricalValuesAsync / SendEventAsync). Producers enqueue writes; the writer persists them and replays on reconnect so a transient disconnect never drops data. This is the roadmap's recommended pragmatic outbox, NOT a bit-faithful reimplementation of AVEVA's native SF cache (that stays deferred) — pure managed, no RE. - HistorianOutboxEntry / HistorianOutboxEntryKind: buffered-write envelope - IHistorianOutboxStore + InMemoryHistorianOutboxStore (tests) + FileHistorianOutboxStore (crash-durable: atomic temp+move JSON per entry, FIFO by filename sequence that resumes past on-disk max, corrupt-file quarantine). OutboxJson normalizes event object? properties off JsonElement. - IHistorianWriteSink + HistorianClientWriteSink (HistorianClient-backed) - HistorianStoreForwardWriter: enqueue, single-flight FIFO FlushAsync with head-of-line blocking, optional MaxDeliveryAttempts dead-lettering, DropOldest/Reject overflow policy, background drain loop (retry on reconnect), GetStatusAsync snapshot mirroring server SF Pending/Storing/ErrorOccurred. 12 unit tests (no server): durability-across-restart, reconnect-drain, FIFO order/head-of-line, dead-letter, overflow policies, background auto-drain. Full suite 293 green. Roadmap R4.1 marked shipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
a91f126287 |
docs(hcal-roadmap): M3 R3.2 ships all 5 analog types, not Float-only
R3.2 and the one-glance table still read "Float-only"; the shipped AddHistoricalValuesAsync covers Float/Double/Int2/Int4/UInt4 (golden-tested + live write/read-back). Correct both lines to match code + tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
d1e96f48de |
M3 R3.2: AddHistoricalValuesAsync supports Double + Int (Int2/Int4/UInt4)
Extended the historical-write serializer from Float-only to all five analog types EnsureTagAsync supports. Captured each type's "ON" buffer live from the native client (sandbox tag per type, written + captured + deleted): - The 4-byte value descriptor (C0 10 01 00) is CONSTANT across types — it does not encode the type. - The value is u32(0) + native-width value, width by the tag's declared type: Float->float32, Double->double64, Int2->int16, Int4->int32, UInt4->uint32. HistorianHistoricalWriteProtocol.SerializeAddStreamValuesBuffer now takes the HistorianDataType and encodes accordingly (unsupported types throw ProtocolEvidenceMissingException). The orchestrator resolves the type from the tag-info NativeDataTypeDescriptor via MapDataType. Harness capture-write gained --data-type. Golden-tested against all five live captures + the gated write/read-back test validated each type end-to-end through the pure-managed SDK; 281 unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
dafafa0c98 |
M3 R3.2 SHIPPED: docs — AddHistoricalValuesAsync recorded in roadmap, plan, and CLAUDE.md surface
Marks M3 historical writes SHIPPED + live-validated across the roadmap (R3.2/R3.3/one-glance), revision-write-path.md §"R3.1 CAPTURED", and the CLAUDE.md Required SDK Surface (the new write op, gRPC-only, AddStreamValues "ON" path, Float-only, distinct from the still-blocked AddS2 streaming path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
0e78d638d0 |
M3 R3.1: document the captured + validated AddStreamValues "ON" write path
revision-write-path.md §"R3.1 CAPTURED" + roadmap R3.1/R3.2/one-glance now record the validated
finding: the historical write is HistoryService.AddStreamValues ("ON" storage-sample buffer, AddS2
"OS" family) + EnsureTags, not AddNonStreamValues/TransactionService. Includes the decoded 56-byte
"ON" buffer layout, the working priming/batch sequence, the tag-GUID keying, and that the D2 cache
gate does not block the primed 2023 R2 client. Remaining work to ship AddHistoricalValuesAsync is
the managed "ON" serializer (adapt HistorianEventWriteProtocol) + gRPC orchestrator wiring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
|
||
|
|
222eed9c02 |
M3 R3.1: durable capture plan — drive native 2023 R2 gRPC client + IL-rewrite byte[] payloads
Records the feasibility-verified plan to capture the two remaining buffers (regular-tag RegisterTags btTagInfos + AddNonStreamValues btInput): - 2023 R2 aahClientManaged.dll is self-contained mixed-mode C++/CLI (only Windows + VC++ runtime native imports) — loadable in a net481 x64 process, no AVEVA install needed. - gRPC routes through the managed Archestra.Historian.GrpcClient.dll, so the byte[] payloads are capturable by IL-rewriting GrpcHistoryClient.RegisterTags / AddNonStreamValues (dnlib, the instrument-wcf-writemessage pattern; rewrite a copy, never the originals). - Connection is reflection-drivable: HistorianAccess.OpenConnection(HistorianConnectionArgs) with ConnectionMode=HistorianConnectionMode.Historian (the gRPC mode), TcpPort=32565, cert. - gRPC runtime deps (Grpc.Net.Client / Grpc.Core.Api / Google.Protobuf / ...) are present in msi-extract/ArchestrA/Toolkits/Bin/x64. - Risk: the C++ AddNonStreamedValue TagNotFoundInCache(129) gate (the 2020 D2 blocker) may block btInput; mitigation = read the tag first. RegisterTags is emitted before that gate. Build order documented (read-only connect -> IL-rewrite -> write capture -> serializer -> commit+read-back -> AddHistoricalValuesAsync), each live step gated on per-action auth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
57b9506d01 |
M3 R3.1: OpenStorageConnection is a dead end (error 85); precondition is front-door RegisterTags
Live-probed StorageService.OpenStorageConnection against the 2023 R2 server over a
write-enabled (0x401) session. Every attempt — sweeping ConnectionMode (0x401/0x402/0x1),
StorageSessionId-in (Open2-GUID / empty), and FreeDiskSpace — returns the IDENTICAL native
error type=4 code=85 ("session not registered"), so it's a structural refusal, not a bad
field value.
Decode (two corroborating facts):
- Error 85 is the same code the event read returns before RegisterTags2 (see
HistorianWcfEventOrchestrator) — a generic "session not registered for this op".
- The 2023 R2 decompile shows OpenStorageConnection lives on a SEPARATE GrpcStorageClient
(the storage engine's SF/snapshot channel, own port + service identity); HistorianAccess
drives non-streamed writes through the native C++ HistorianClient, never this op.
So the roadmap's mapped "missing console session" step was wrong. The real non-streamed-write
precondition is the front-door HistoryService.RegisterTags (RTag2-family) for the target tag —
which is exactly why the R3.1 batch failed at AddNonStreamValues (no tag registered ->
StoreNonStreamValues had no route). Matches the original 2020-WCF D2 hypothesis.
Remaining (both need a native gRPC capture; do not guess bytes): the regular-tag RegisterTags
btTagInfos (only CM_EVENT's tag-GUID form is known) and the AddNonStreamValues btInput.
- HistorianGrpcStorageConnectionProbe + grpc-open-storage-connection CLI (opens nothing
persistent; CloseStorageConnection on success)
- corrected revision-write-path.md §R3.1 follow-up + hcal-roadmap R3.1/R3.2 rows
- gated regression test pinning the error-85 refusal
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
|
||
|
|
1a08dd9ec2 |
M3: roadmap reflects mapped non-streamed sequence (OpenStorageConnection follow-up)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
ac28679a1f |
M3 R3.1: map the required non-streamed write sequence (OpenStorageConnection is the missing step)
Static decompile mining of the 2023 R2 client corroborates the live R3.1 error: the AddNonStreamValues failure is the missing StorageService.OpenStorageConnection, which creates exactly the \.\pipe\aahStorageEngine\console,sid(...) session named in the server error. Mapped the full native sequence: HistoryService.OpenConnection (have) -> StorageService.OpenStorageConnection (MISSING) -> StorageService.RegisterTags -> AddNonStreamValuesBegin (works) -> AddNonStreamValues(btInput) (fails - no console session) -> End(commit). Two hard parts remain, each a live-production decode loop with no static shortcut: (1) reproduce the 12-arg OpenStorageConnection handshake (several args inferred); (2) decode the AddNonStreamValues btInput (C++-built, absent from decompiles; only the 44-byte packed HISTORIAN_VALUE2 is known). Documented in revision-write-path.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
8fbb868813 |
M3 R3.1 decode: AddNonStreamValues reaches server StoreNonStreamValues (storage-engine console pipe)
Empirically decoded the AddNonStreamValues btInput framing against the live 2023 R2 server (grpc-nonstream-decode command + ProbeNonStreamedBuffersAsync driver). Every transaction rolled back (bCommit=false) — no data written. Finding: the btInput is assembled native-C++-side (not in any decompile), so 6 evidence-based framings (44-54B, packed HISTORIAN_VALUE2 variants) were probed. All 6 returned the IDENTICAL server error while an empty buffer returned a different InvalidParameter — so non-empty buffers pass parameter validation into CHistStorageConnection::StoreNonStreamValues, which routes to the \.\pipe\aahStorageEngine\console pipe server-side. Identical-across-framings => the blocker is NOT the btInput layout but a missing storage-engine console session / tag-registration precondition for the connection. Next step (untested): StorageService.OpenStorageConnection + tag registration (RegisterTags/AddTagidPairs/AddShardTagids) before AddNonStreamValues, then commit + read-back on a sandbox tag. Documented in revision-write-path.md (R3.1 decode section); raw artifact gitignored. 272 unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
23798db1ef |
M3 probe: non-streamed write transaction reachable over 2023 R2 gRPC (Begin/End live-verified)
The D2 storage-engine-pipe wall is WCF-transport-specific. On the 2023 R2 gRPC front door, TransactionService is a first-class service AND the gateway to the storage engine, so the Open2 storage-session GUID (uppercase) is accepted directly as strHandle with no legacy pipe. Live-verified against the real 2023 R2 server over a write-enabled (0x401) gRPC session: AddNonStreamValuesBegin returns a real strTransactionId, and AddNonStreamValuesEnd(bCommit=false) discards it cleanly (no data written). On 2020 WCF the same op returns UnknownClient(51) for every handle + priming chain. - HistorianGrpcRevisionProbe + grpc-revision-probe CLI command + gated test NonStreamedWriteTransaction_OverGrpc_BeginsAndDiscards (live pass). - HistorianGrpcHandshake.OpenSession gains an optional connectionMode param (default read-only 0x402; pass 0x401 for write-enabled) — non-breaking. - Docs: revision-write-path.md "the wall is gone" section; roadmap M3 section, R3.1-R3.3 rows, one-glance table, and status note updated honestly. Not yet shipped: the AddNonStreamValues btInput VTQ buffer is uncaptured (never guess wire bytes), so no value-commit is implemented. Scope is non-streamed ORIGINAL backfill; revision EDITS (R4.2) remain pipe-only even on gRPC. 272 unit tests pass; sanitization scan clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
04ea0b9a1f |
R1.3 GetServerTimeZoneAsync over gRPC (live-verified); R1.4 bounded out on gRPC
Live-probed both R1.3 and R1.4 against a real 2023 R2 server over the gRPC StatusService; implemented the one that carries an evidence-backed value. R1.3 GetServerTimeZoneAsync — SHIPPED: - StatusService.GetSystemTimeZoneName(uiHandle) returns the real server zone over RemoteGrpc (the 2020 WCF op is a client-side stub returning empty). - HistorianGrpcStatusClient.GetSystemTimeZoneNameAsync -> dialect routing -> public HistorianClient.GetServerTimeZoneAsync. Non-gRPC transports fail closed with ProtocolEvidenceMissingException (no empty-string lie). - Golden message-shape unit test + non-gRPC guardrail unit test + gated live test. 271 unit tests pass. R1.4 GetHistorianInfoAsync (EventStorageMode) — bounded out on gRPC too: - gRPC GetHistorianInfo is the same named-value query as 2020 WCF (only HistorianVersion resolves); EventStorageMode + 7 variants fail on both GetHistorianInfo and GetSystemParameter. The 518-byte struct is filled by a native vtable+648 HCAL call, not the gRPC op (per the 2023 R2 decompile), so the field is never on the wire. Not shipped on any transport. Closes the roadmap's open "build against a live 2023 R2 server" caveat. Also correct the stale M3 roadmap section: D2 already proved Transaction.AddNonStreamValues* rides the storage-engine pipe (STransactPipeClient2 -> aaStorageEngine), not WCF — same wall as R4.2 — so M3-over-WCF is blocked, not "the path that is NOT the gated cache push". Docs: hcal-roadmap.md, wcf-historian-info.md, wcf-status-localhost.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
25aff409dc | Merge re/grpc-2023r2-handshake: M0 gRPC parity (probe/system-param/metadata/browse) + handshake fix | ||
|
|
4de222c950 |
Merge re/r1.4-gethi-finding: R1.1 ExecuteSqlCommand + R1.4 GetHistorianInfo (bounded)
# Conflicts: # docs/plans/hcal-roadmap.md # src/AVEVA.Historian.Client/HistorianClient.cs # src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs # tests/AVEVA.Historian.Client.Tests/HistorianClientIntegrationTests.cs # tools/AVEVA.Historian.NativeTraceHarness/Program.cs |
||
|
|
85ff1b48df |
R0.1 browse over gRPC SHIPPED — QueryTag cracked, M0 gRPC parity complete
Wires HistorianClient.BrowseTagNamesAsync over gRPC (Transport==RemoteGrpc) via
Grpc/HistorianGrpcTagClient.BrowseTagNamesAsync: StartTagQuery(OData) -> paged
QueryTag -> EndTagQuery. Live-verified against a real 2023 R2 server (returns Sys* tags).
QueryTag packet-id recovered WITHOUT native disassembly: a .rdata packet-descriptor
table in aahClientManaged.dll lists {0x6751,1}=StartTagQuery immediately followed by
{0x6752,1}=QueryTag (found via pefile byte-scan of .rdata), confirmed live.
Wire format (live-verified):
- request btRequest = u16 0x6752 + u16 version(1) + u16 queryType(1=names) + u32 startIndex + u32 count
- response btResonse = u32 count + per-name(u32 charCount + UTF-16LE) + trailer (NextIndex/metadata, ignored)
- new HistorianTagQueryProtocol.ParseTagNameQueryPage tolerates the trailer
- GlobToODataFilter translates the SDK glob filter to OData (Pre*->startswith, *suf->endswith,
*mid*->contains, exact->eq); the 2023 R2 metadata-server parses filters as OData.
Replaces the earlier RE probe helpers with the shipped browse path. Adds golden-byte
(BuildQueryTagRequest) + 8 glob-translation unit tests + gated live browse test.
226 unit tests pass; 5/5 live gRPC tests pass (read, probe, system-param, metadata, browse).
Milestone 0 (full gRPC parity) is complete.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
|
||
|
|
26ef5e5645 |
R0.1 browse probe: StartTagQuery over gRPC takes an OData filter (live)
Probes the 2023 R2 gRPC browse path and records the finding. The front door does NOT hit the 2020 WCF metadata-server-pipe wall. - RetrievalService.StartTagQuery is cracked: the server (CMdServer::StartActiveTagnamesQuery over \.\pipe\aahMetadataServer\console) parses the filter as OData. startswith()/ contains()/eq/empty succeed and return the 8-byte (queryHandle, tagCount); SQL-LIKE "%" and glob "*" fail with "ODataFilter: bad token". Live: 220 Sys* tags counted. - QueryTag (paging) remains: every guessed btRequest returns a constant native error type 4 / code 72 (content-independent) -> framing needs a native capture, not guessing. Adds RE probe helpers Grpc/HistorianGrpcTagClient.ProbeStartTagQuery + ProbeTagQuerySequence, a gated StartTagQuery_OverGrpc_AcceptsODataFilter test, and the finding doc docs/reverse-engineering/grpc-tag-query-odata.md. Browse is not yet wired (QueryTag open). 217 unit tests pass; 5/5 live gRPC tests pass. No tag names/identities committed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
0e19adae68 |
gRPC M0 R0.2: tag metadata over gRPC (GetTagInfosFromName, live-verified)
Routes HistorianClient.GetTagMetadataAsync over gRPC when Transport==RemoteGrpc,
via the new Grpc/HistorianGrpcTagClient calling RetrievalService.GetTagInfosFromName
(the plural string-handle metadata op).
- String handle = the Open2 storage-session GUID formatted uppercase (the format
that resolves the native string-handle path); threaded out of the shared handshake
via a new HistorianGrpcHandshake.Session { ClientHandle, StorageSessionId, StringHandle }.
- Request btTagNames = uint count + per-name(uint charCount + UTF-16LE) — golden-byte
unit-tested (BuildTagNamesBuffer).
- Response btTagInfos = uint count + CTagMetadata records — decoded by the existing
HistorianTagQueryProtocol.ParseGetTagInfoResponse; data type via the shared MapDataType.
The 2020 WCF string-handle wall does NOT apply on the gRPC front door, as the
string-handle-wall RE note predicted. LIVE-VERIFIED against a real 2023 R2 server:
GetTagMetadataAsync returns the requested tag with a valid decoded data type.
216 unit tests pass. Captured framing confirmed live then discarded; no tag names
or identities committed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
|
||
|
|
b0703ebf80 |
docs: R0.3 live-verified; correct the auth-blocker note (harness quote bug)
R0.3 system-param over gRPC is now LIVE-VERIFIED against the real 2023 R2 server (returned HistorianVersion), alongside the re-confirmed read chain and probe. The apparent NTLM round-1 SEC_E_LOGON_DENIED "blocker" was a test-harness credential-parsing bug, not a server/account/SDK issue: the gitignored creds file stores quoted values and the env-setup must strip surrounding quotes before exporting HISTORIAN_USER/PASSWORD. With quotes stripped, the NAM domain account authenticates and the full chain passes. The round-failure diagnostic added during the hunt (HistorianNativeHandshake.DescribeError) is kept. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
c4b8d0dde4 |
gRPC M0: probe (R0.4, live-verified) + system-param (R0.3) + shared handshake
Roadmap docs/plans/hcal-roadmap.md, milestone M0 (gRPC parity for the DONE surface). Now unblocked for live verification by a reachable 2023 R2 server. - R0.4 Probe over gRPC: new HistorianGrpcProbe calls History/Retrieval/Status GetInterfaceVersion (unauthenticated). ProbeAsync routes over gRPC when Transport==RemoteGrpc. LIVE-VERIFIED against a real 2023 R2 server — needs no credentials (runs before the auth loop), so it works despite the auth blocker. - R0.3 System parameter over gRPC: new HistorianGrpcStatusClient calls StatusService.GetSystemParameter over the authenticated session; routed in the dialect. Built + unit-tested (request/response field mapping pinned). Live-verification pending an auth fix (see below). - Extracted the proven auth handshake from HistorianGrpcReadOrchestrator into shared Grpc/HistorianGrpcHandshake (reused by read + status + future browse/metadata). Repointed the IL structural guardrail test to it. - Diagnostics: round-failure now decodes the native server error + hex/ASCII preview (HistorianNativeHandshake.DescribeError). This surfaced the live auth blocker as SEC_E_LOGON_DENIED (0x8009030C) at NTLM round 1 — framing is correct, the credential did not validate. Probable cause: stale file password or NAM-domain NTLM restriction (Kerberos/RDP works, NTLM denied; no SPN path over the tunnel). 216 unit tests pass; live gRPC probe passes. Sanitization scan clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
c1b1b3d23b |
R1.11 DelTep capture + R1.3/R1.4/R1.12/R1.13 bounded out
DelTep (extended-property delete) — wire format captured + serializer golden-proven, but live delete is server-blocked and NOT exposed publicly: - Captured the DelTep inBuff via a cross-session trick (harness add-tep gains --tep-skip-add + read-for-sync before --tep-delete; Capture-DeleteTagExtended Properties.ps1 / decode-del-tep-capture.py). Layout = same group framing as AddTEx but property-name-only (no 0x43 value) + 0x00 group trailer. - SerializeDeleteRequest + 4 golden tests pin the server-accepted buffer. - A decisive experiment shows SDK-added properties ARE deletable (the native client read-syncs and deletes one), so SDK-add is complete; the SDK's own DelTep is rejected by CHistStorage::DeleteTagExtendedProperties even with byte-identical inBuff, matching mode/handle, GetTgByNm+GetTepByNm prime, open channel, and 60s retries. Root cause: the native multiplexes services over one connection (per-connection working set); the SDK's per-service WCF channels don't reproduce it. Kept as documented-but-blocked internal orchestrator path; no public HistorianClient delete API. Bounded out with evidence (no code; docs + roadmap + probe): - R1.12 localized-property write — no op on 2020 (mirror of R1.6); no *LocalizedPropert*/TagLocalized* symbol in any current/*.dll. - R1.13 non-analog tag create — GATED; native AddTag rejects every non-analog type client-side (ValidationFailed, before any WCF op): SingleByteString, DoubleByteString, Int1 all fail, Float works. No Discrete type in the native enum, no TagType setter. No wire request to capture. - R1.3 timezone + R1.4 EventStorageMode — re-confirmed 2023R2/gRPC-only from the Runtime DB schema (no timezone param, no EventStorageMode anywhere) and a parameter-op probe (GetSystemParameter + GETRP return null/throw for every candidate; only HistorianVersion works). 238 unit tests pass; full solution builds with 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
08b950caee |
R1.11 AddTagExtendedPropertiesAsync: extended-property write via AddTEx
Adds user-defined extended properties to an existing tag via the 2020 WCF
AddTEx (AddTagExtendedProperties) op. Write-enabled connection + uppercase
storage-session GUID handle; reuses the write orchestrator open/priming chain.
The AddTEx inBuff is the exact inverse of the R1.5 GetTepByNm read-response
framing, so the serializer mirrors the read parser:
uint32 groupCount + 0x01(group) + [0x09+u16+ASCII tag] + uint32 propCount
+ per prop{ 0x02 + [0x09+u16+ASCII name] + 0x43 VT_BSTR + u16 payloadLen
+ u16 charCount + UTF-16 value } + 0x01(group trailer) + 0x00(terminator).
The trailing 0x00 is required — without it inBuff is one byte short and the
server throws SErrorException in CHistStorage::AddTagExtendedProperties. The
golden fixture pins the clean inBuff the live server accepted (dumped via
AVEVA_HISTORIAN_TEP_DUMP); read-back verified via R1.5. String (0x43) values only.
Delete (DelTep) is deferred: the native DeleteTagExtendedPropertiesByName does a
client-side sync check and returns err 229 for a just-added property, so the
DelTep request never reaches the wire and its inBuff can't be captured yet.
Shipped: HistorianClient.AddTagExtendedPropertiesAsync/AddTagExtendedPropertyAsync;
HistorianTagExtendedPropertyProtocol.SerializeAddRequest; orchestrator path;
golden WcfTagExtendedPropertyWriteProtocolTests (4); gated live write/read-back test;
native-harness `add-tep` scenario + Capture-AddTagExtendedProperties.ps1 +
decode-add-tep-capture.py. Doc: wcf-add-tag-extended-properties.md. 233 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
|
||
|
|
fbd839077b |
R1.4 GetHistorianInfo: bounded out on 2020 WCF (named-value-only, no struct)
Captured the native HistorianAccess.GetHistorianInfo(out HistorianInfo, out err) and decoded the wire: over 2020 WCF, GETHI is a named-value query whose only working key is "HistorianVersion" (response ~30 bytes = the version string). Probed 7 storage-mode key names -> all ok=False/err. The 518-byte HISTORIAN_INFO struct + EventStorageMode@514 is the 2023R2 HCAL-native/gRPC model (confirmed from the decompiled 2023R2 source); on 2020 the native client derives the mode outside the WCF wire. Version is already exposed (ProbeAsync/GetRuntimeParameterAsync), so no hollow GetHistorianInfoAsync is shipped (same disposition as R1.3 timezone). This completes the reachable 2020-WCF M1 read surface; remaining M1 = config writes (gated on explicit request) or gRPC/2023R2-only items. RE aids kept: harness `historian-info` scenario, Capture-HistorianInfo.ps1, decode-historian-info-capture.py, and StringHandleProbeDiagnosticTests .GETHI_CandidateInfoNames (asserts the named-value-only finding; gated). Docs: wcf-historian-info.md (new) + roadmap/matrix/wall-doc updates. 230 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
1a539882d0 |
R1.1 ExecuteSqlCommandAsync (ExeC + GetR, NRBF DataTable, no BinaryFormatter)
Ship SQL command execution over the 2020 WCF aa/Retr/ExeC + aa/Retr/GetR ops: HistorianClient.ExecuteSqlCommandAsync(sql) -> HistorianSqlResult (columns + typed rows). String-handle ops reached with the Open2 storage-session GUID formatted uppercase (the handle format that unlocked GETRP/GETHI). Chain: Retr.GetV prime -> ExeC(handle, sql, option=0, ref queryHandle) -> GetR loop. Key gotcha captured: GetR returns FALSE even on success -- the byte stream is in pResultBuff regardless; false just signals the final page. So the orchestrator consumes the buffer first, then stops on a false result / empty page. GetR's pResultBuff is an NRBF-serialized System.Data.DataTable (SerializationFormat.Xml: members XmlSchema (XSD) + XmlDiffGram (rows)). BinaryFormatter is removed from .NET 10, so the stream is decoded read-only with the System.Formats.Nrbf package (NrbfDecoder) + XDocument -- no BinaryFormatter, no code execution. Values are typed per the XSD type, falling back to string. Adds: HistorianSqlResult / HistorianSqlColumn / HistorianSqlExecuteOption models, HistorianSqlResultProtocol (NRBF + diffgram parser), HistorianWcfSqlClient (ExeC/GetR orchestration with an AVEVA_HISTORIAN_SQL_DUMP diagnostic), dialect + public API. Golden WcfSqlResultProtocolTests pinned to the real clean GetR stream for the benign "SELECT 1 AS ProbeValue" (no sensitive data); gated live tests (single cell + multi-column/multi-row/NULL). Doc: wcf-exec-sql.md; roadmap R1.1 DONE; wall doc + memory updated (incl. the QTB-server-side nuance). 229 tests green. Note: a raw instrument-wcf capture corrupts a large pResultBuff with MDAS transport chunk markers (0x9F); the clean contract-level byte[] is dumped via the AVEVA_HISTORIAN_SQL_DUMP env var for the golden fixture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
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 |
||
|
|
4da5287d01 |
R1.2 GetRuntimeParameter + string-handle wall RESOLVED (handle-format bug)
Execute HCAL roadmap R1.2 (GetRuntimeParameterAsync) end-to-end, and in doing so
discover that the "string-handle wall" blocking R1.1/R1.4/R1.5/R1.6 was a handle
FORMAT bug, not a missing native session/filter registration.
R1.2 (shipped, live-verified):
- Captured native GetRuntimeParameter -> WCF op aa/Stat/GETRP (string-handle op,
GETHI's shape), via scripts/Capture-RuntimeParam.ps1 + instrument-wcf-{write,read}message.
- HistorianRuntimeParameterProtocol serializes pRequestBuff (54 67 01 00 + uint
nameCount + per-name uint charCount + UTF-16) and parses pResponseBuff (version +
uint resultCount + CRetVariant 0x43 VT_BSTR + uint16 len + uint16 charCount + UTF-16).
- IStatusServiceContract2.GetRuntimeParameter (GETRP) op; HistorianWcfStatusClient
passes the Open2 storage-session GUID as the string handle, UPPERCASE.
- Public HistorianClient.GetRuntimeParameterAsync(name) via the dialect.
- Golden WcfRuntimeParameterProtocolTests + gated live test; returns HistorianVersion.
String-handle wall RESOLVED (proven, public APIs deferred):
- The Open2 storage GUID works as the string handle when sent UPPERCASE
(ToString("D").ToUpperInvariant()); earlier "blocked" probes used lowercase.
- Live-probed GETHI (R1.4) -> returns data; ExeC (R1.1) -> Retr.GetV prime -> ExeC ->
GetR returns a BinaryFormatter-serialized .NET DataTable. Gated
StringHandleProbeDiagnosticTests + scripts/Capture-ExecSql.ps1 + exec-sql harness scenario.
- Docs flipped: wcf-string-handle-wall.md RESOLVED banner; roadmap R1.1/R1.4 reachable,
R1.5/R1.6 likely; wcf-status-localhost.md GETRP section.
- R1.1/R1.4 public APIs NOT shipped: ExeC needs a GetR paging loop + a BinaryFormatter-
stream parser (BinaryFormatter is removed from .NET 10); GETHI full-info struct needs
its own capture.
223 unit tests pass; gated live tests green against the local 2020 Historian.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
|
||
|
|
6d470eab4a |
R1.7: server-side event filters — ReadEventsAsync(HistorianEventFilter), live-honored
Roadmap M1 R1.7. Filters are set on the native EventQuery object via AddEventFilter(property, HistorianComparisionType, value) — NOT EventQueryArgs (time/count/order only). Found via a new harness --dump-type-members command. Captured the native filtered StartEventQuery pRequestBuff (Capture-EventFilter.ps1 + harness --event-filter knob) and diffed Equal(0) vs Contains(12) to isolate the operator field. Filter block (decoded byte-for-byte): ushort 0 + uint filterCount + uint condCount + uint nameLen + name(UTF-16) + uint 1 + ushort op + uint 1 + value(0x09-LEN-0x00 compact-ASCII) + byte 0 The filter is REAL, not inert (unlike the analog-summary knobs): a non-matching predicate returns 0 events; Type=Equal=User.Write returns only User.Write events. Verified live via both the native harness and the SDK. - HistorianClient.ReadEventsAsync(start, end, HistorianEventFilter, ct) overload - HistorianEventFilter + HistorianEventComparison (18 ops, ordinals = native) - Filter encoding in HistorianEventQueryProtocol (empty-filter path unchanged) - Golden-byte tests (block match, op field, empty-filter regression) + gated live test Single string-valued predicate only; multi-filter (OR) / multi-condition (AND via AddEventFilterCondition) framing is partially captured and not shipped. 216 unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
f1e23a3a02 |
M2: implement SendEventAsync — event-send rides WCF AddS2, not the storage pipe
Roadmap Milestone 2 (event sending). Capture disproved the assumption that event delivery uses the non-WCF storage-engine pipe (which would block it like revision writes): a native AddStreamedValue(HistorianEvent) leaves over WCF as AddS2 (IHistoryServiceContract2.AddStreamValues2). CM_EVENT is a built-in registered tag, so the 129 TagNotFoundInCache gate that blocks AddS2 for user tags does not apply. - R2.1: NativeTraceHarness "event-send" scenario + Capture-EventSend.ps1; two captures diffed to separate constant framing from value-dependent fields. - R2.2: HistorianEventWriteProtocol serializes the AddS2 pBuf (storage sample buffer wrapping the event VTQ) — golden-byte tested. Decoded "OS" sig + length fields + CM_EVENT tag id + EventTime/ReceivedTime FILETIMEs + Opc 192 + 0x118D descriptor + event Id + Namespace + EventType + version 5 + typed property bag. - R2.3/R2.4: HistorianWcfEventOrchestrator.SendEventAsync (Open2 event-mode 0x501 -> reuse CM_EVENT RTag2/EnsT2 -> AddStreamValues2) + HistorianClient.SendEventAsync. - R2.5: gated live test; server accepts the AddS2 (success, empty error buffer). Server requires delivered byte[].Length == declared packet length (uint32@0x04); the native relies on the MDAS encoder adding a pad byte, so the SDK emits an explicit trailing 0x00 (else AddS2 rejects with "CValuStream buffer size vs packet length mismatch"). Original events only (RevisionVersion=0) with string properties; other property types + revision/update/delete throw ProtocolEvidenceMissingException. Caveat (documented): accepted events are not persisted on the local dev box; the native client behaves identically (event ingestion pipeline inactive) — not an SDK gap. 212 unit tests pass; 16/16 event tests pass live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC |
||
|
|
1a7519c803 |
RE: resolve R1.8/R1.9 analog/state summary via request+response capture
Captured the native StartQuery2 pRequestBuff and the GetNextQueryResultBuffer2 response (instrument-wcf-writemessage + chained instrument-wcf-readmessage) and decoded both against AnalogSummaryHistory SQL ground truth. Conclusion: the rich multi-aggregate analog/state summary struct is NOT delivered over the 2020 WCF binary protocol — the response is the ordinary version-9 row buffer the existing aggregate parser already handles, carrying one value per cycle selected by RetrievalMode (QueryType 5-8), not ValueSelector (inert on this path). So "analog summary" == the existing ReadAggregateAsync; no new src/ code warranted. Tooling (tools/ + scripts/ only, nothing in src/): - NativeTraceHarness: drive summary knobs via --value-selector / --aggregation-type / --max-states (uint16) / --filter - Capture-SummaryRequest.ps1: repeatable instrument+stage+matrix capture, -WithResponse chains the ReadMessage hook - decode-summary-capture.py: StartQuery2 request diff vs baseline - decode-summary-response.py: response decode vs SQL ground truth Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
34e352ba28 |
R1.8/R1.9: empirical summary-query probe + enum-dump RE command
Pushed on recovering the summary query params. Findings: - Added `enum-dump` to the RE CLI (dumps a managed enum's literal members). Confirmed INSQL_QUERYTYPE / HISTORIAN_SUMMARYTYPE are value__-only in the managed metadata — their named members are native-side constants, so they can't be recovered statically. Same for CColumnNameMap.LoadColumnNameMap (column->bit built from native string/const data, not IL ldstr/ldc). - Live-probed StartQuery2 against SysTimeSec sweeping QueryType/SummaryType/ ColumnSelectorFlags. The server ACCEPTS SummaryType 1/2/4/5 (returns a valid version-9 buffer) but yields 0 rows; column flags don't change that; QueryType 15/16 don't exist. So a summary query is NOT Full+SummaryType+ flags — the config lives in the AutoSummaryParameters trailer (currently zeroed) and/or a native summary QueryType. Conclusion recorded in the plan: the request shape needs a NATIVE capture (instrument-wcf-writemessage on a real summary query), not managed-metadata recovery or blind probing. Decode targets remain located. No guessed code in src/; probe scaffolding removed. 208 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
085f01123c |
docs: scope R1.8/R1.9 summary queries (decode targets located)
Established that analog/state-summary queries are reachable on 2020 WCF — they
ride the proven uint-handle StartQuery2 path, and the request serializer already
carries QueryType/SummaryType/ColumnSelectorFlags. Located every decode target in
aahClientManaged.dll:
- CAnalogSummaryValue.UnpackFromValueBuffer (0x06000394) — row decoder
- CAnalogSummaryValue/Struct fields — Min/Max/First/Last/ValueCount/TimeGood/
Integral/IntegralOfSquares (+ per-field DateTimes, LinearIntegral)
- CStateSummaryStruct — MinContained/MaxContained/TotalContained/PartialStart/
PartialEnd/StateEntryCount
- QueryColumnSelector.Select{Analog,State,NonSummary}Columns — column flags
- INSQL_QUERYTYPE / HISTORIAN_SUMMARYTYPE — the query/summary enum values
UnpackFromValueBuffer is reader-call-based (no literal offsets), so a correct
parser needs a captured real buffer. Per project discipline no guessed summary
code was added to src/. New plan doc lays out the recover-params -> live-capture
-> decode -> implement+test path. Roadmap R1.8/R1.9 marked scoped/ready.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
84ec175f76 |
docs: map the 2020 WCF string-handle wall (R1.4 GETHI blocked)
Probed R1.4 GetHistorianInfo (GETHI) live against the local 2020 server. GETHI returns native error type 4 / code 1 for the exact native request shape across 5 handle formats (storage GUID, context GUID, uint decimal/X8/0x-hex) even with Stat.GetV ×2 priming. Its result is discarded (TryRun) in the only place it's used, so it was never actually verified to return data managed-side. This confirms a structural boundary on the 2020 WCF surface: ops taking a uint client handle work (the proven read/browse/metadata/status/event surface); ops taking a string GUID handle (ExeC, QTB, QTG, GETHI, GetTepByNm, ...) are blocked behind an unmapped native session/filter registration. Every remaining M1 *read* item (R1.1/R1.4/R1.5/R1.6) is string-handle -> all gated on that one RE target. Reachable uint-handle items: R1.7 event filters, R1.8/R1.9 summary modes. New: docs/reverse-engineering/wcf-string-handle-wall.md (full dichotomy + table). Roadmap R1.4/R1.5/R1.6 struck through; reachable items re-pointed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2246fdd395 |
docs: reclassify M1a R1.1/R1.3 as blocked on 2020 WCF
Live-probed both against the local Historian 2020 (WCF): - R1.3 GetServerTimeZoneAsync: Status.GetSystemTimeZoneName returns rc=0 with an empty value under a real authenticated handle — a client-side stub in the GetServerTime family. gRPC/2023R2-only. Reverted the implementation. - R1.1 ExecuteSqlCommandAsync: Retrieval.ExeC returns native error type 4 / code 51 (InvalidParameter); the contract-3 string-handle ops require an unmapped native session/filter registration step (the StartTagQuery wall). Adds an M1a re-classification note steering future work toward proven uint-handle / already-wired ops (R1.4 GETHI next) over string-handle ops. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cf5a66e046 |
docs/plans: mark R0.6 + CW-1 done; note 2020-only live-verification constraint
The local Historian is 2020 (WCF/32568); the 2023 R2 gRPC endpoint (32565) is absent, so M0 gRPC routing can be unit-tested but not live-verified here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a530ae0f10 |
docs/plans: import 2023 R2 gRPC analysis + HCAL reimpl roadmap
Version-control the planning docs alongside the code they describe: - grpc-transport.md — 2023 R2 gRPC transport analysis (sanitized source path) - hcal-capability-matrix.md — HistorianAccess surface x gRPC ops x histsdk status x feasibility tiers - hcal-roadmap.md — ordered build plan M0-M4 + cross-cutting workstreams - histevents.md — how a HistorianEvent reaches the DB (client->wire->server) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8a553423ed |
D2: definitive conclusion — revision-write requires non-WCF storage-engine pipe
IL walk of the native wrapper:
HistorianAccess.AddRevisionValuesBegin (private, token 0x06006175)
-> CClientCommon.AddNonStreamValuesBegin
-> CClient.AddNonStreamValuesBegin (8-instr overload)
-> CClient.TransactionBegin
-> CHistStorageConnection.StartTransaction (token 0x06001FDD)
-> CStorageEngineConsoleClient.StartTransaction
CStorageEngineConsoleClient is built on STransactPipeClient2 +
SCrtMemFile — a shared-memory + named-pipe transport to
aaStorageEngine.exe, completely separate from WCF.
The WCF ITransactionServiceContract2.AddNonStreamValuesBegin2 op is a
server-side relay that requires a pre-existing storage-engine pipe
session for the client. Without that pipe session, the WCF relay returns
UnknownClient (51) — and there's no way to establish the pipe session
via WCF.
D2 is unimplementable as a pure-managed-WCF SDK. The native wrapper
itself depends on the C++ shared-memory channel; replicating that from
managed code would require implementing the storage-engine pipe
protocol, which is a major undertaking and out of scope.
The ITransactionServiceContract2 declaration in our contracts file
stays as documentation; no public API or orchestrator added.
HistorianWcfRevisionOrchestrator remains as an internal probe /
regression check — re-run the probe test if anyone believes the
architecture has changed.
178/178 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6b385441c1 |
D2 follow-up: RTag2 doesn't cascade client identity to Trx
Tested hypothesis (1) from the plan: add RTag2(CM_EVENT tag id) to the priming chain before AddNonStreamValuesBegin2. Result: - RTag2 itself succeeds: returns 25-byte response (01000000000100000001EE39C30EDCDC010100000000000000), no error buffer. - But AddNonStreamValuesBegin2 still fails with the same 04 33 00 00 00 (UnknownClient = 51) for all four handle formats. So RTag2 on /Hist isn't the cross-service registration trigger we need for /Trx. Plan doc updated with the result + next-session ordered probes (try IStorageServiceContract, then IL walk CClientCommon, then server-side decompile as last resort). Probe orchestrator now also performs the RTag2 step so the test gives one-shot diagnostic visibility of both calls. 178/178 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b40e6948e2 |
D2 (new path): SDK-direct WCF revision orchestrator + probe
Implemented HistorianWcfRevisionOrchestrator that talks WCF directly to /Trx, bypassing the native wrapper entirely. Probes AddNonStreamValuesBegin2 against the live local Historian and surfaces what the server returns. Internal-only API; no public surface added — the path isn't viable yet. Findings (live test against localhost): - ✅ The wire path is reachable. After moving from V1 (uint handle, no errorBuffer) to V2 (string handle GUID, out errorBuffer), the server recognizes the call (no ContractFilter mismatch, no exception). - ✅ Server processes the call and returns a structured 5-byte error buffer: 04 33 00 00 00 = type 4 (CustomError) + code 51 (UnknownClient). - ❌ Tried four handle formats (contextKey upper/lower, storageSessionId upper, ClientHandle as decimal string) — all return the same UnknownClient. - ❌ Adding the full priming chain (Stat.GetV ×2, Stat.GETHI ×2, UpdC3, 6× Stat.GetSystemParameter, AllowRenameTags, Trx.GetV, Stat.GetV, Retr.GetV) — same result. ITransactionServiceContract2 has no Validate/Register/Open op of its own. The client-with-Trx registration must happen via some cross- service side effect we haven't isolated. Important takeaway: the wire-format mismatch is solved (contract method names + parameter shapes match what the server expects). The remaining gap is a single missing initialization step. Documented in docs/plans/revision-write-path.md as concrete next-session steps. 178/178 tests pass (one new probe test added). Probe is gated on HISTORIAN_HOST=localhost. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b5e5f5485b |
D2: gate is in the C++ HistorianClient, not the managed wrapper
Direct HistorianAccess.AddNonStreamedValue (the 4-param overload that bypasses HistorianDataValueList and goes straight to HistorianClient.AddNonStreamedValueAsync) ALSO fails with 129 TagNotFoundInCache against SysTimeSec, even with validate=false. So the cache check is inside the native C++ HistorianClient's per-connection tag list — there's no managed-callable bypass. Critical insight discovered: the SDK doesn't use the C++ HistorianClient at all. It talks WCF directly. The cache gate that blocks the native wrapper may not block a managed WCF client because the gate is enforced by aahClientManaged, not by the WCF server. This shifts the recommendation for any future D2 attempt from "wrap the native API" (which is genuinely blocked) to "implement the wire path directly on top of the existing ITransactionServiceContract methods and test against the live server" (unverified but plausibly viable). The harness can't help with that path — the wrapper itself is the blocker we'd be bypassing. 177/177 tests still pass; harness gains --write-revision-direct flag for further probing of the native-wrapper path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3af8a13059 |
D2 (revision-write): probe SysTimeSec — same gate, narrower scope
Extended the harness with --write-revision-target-tag <name> (overrides the value's TagKey via SQL lookup) and --write-revision-skip-validate (passes false to AddNonStreamedValue's `validate` boolean). Added --write-revision-commit gate so the harness validates without actually calling SendValues by default — important when targeting system tags. Probed SysTimeSec (wwTagKey=12, server-cache-resident system tag): - AddNonStreamedValue: ErrorCode=TagNotFoundInCache (129) — same failure - With validate=false: same failure (the cache check is intrinsic, not gated by the boolean) Conclusion: the gate is per-(client-session, tag), not per-server-cache. Even tags the SERVER cache knows about are rejected because the LIBRARY maintains a separate per-connection tag list that AddNonStreamedValue checks. That list isn't populated by knowing the wwTagKey alone — it needs whatever mechanism (RegisterTags2 / read flow side effect / IO server registration) that we haven't reverse-engineered. The revision-write path remains architecturally blocked for managed clients. Plan doc updated with the SysTimeSec finding. 177/177 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2feb56d52c |
D2 (revision-write): empirically blocked by same gate as AddS2
Drove the revision-write flow via reflection in the native trace harness (--write-revision-values) to see whether it bypasses the AddS2 architectural blocker. It doesn't. Findings: - HistorianAccess.CreateHistorianDataValueList(NonStreamedOriginal) succeeds - HistorianDataValueList.NonStreamedValuesBegin() succeeds (batchID 0->1) - HistorianDataValueList.AddNonStreamedValue(value, validate=true, out err) FAILS with ErrorCode=TagNotFoundInCache (129) — same client-side validation gate that blocks AddS2 - AddNonStreamedValuesEnd() returns void; SendValues() returns true with Success because the list is empty (no value was ever added) - No AddNonStreamValues* WCF calls reach the wire Conclusion: the revision-write path requires the tag to be in the library's runtime tag cache, which is only populated by configured IO server / Application Server pipelines, not by HistorianAccess.AddTag. This matches the architectural blocker documented for AddS2 and means no public WriteRevisionsAsync / BeginRevisionAsync should be added to the SDK — the path is unreachable for client-created sandbox tags. The Wcf/Contracts/ITransactionServiceContract methods (AddNonStream- ValuesBegin/AddNonStreamValues/AddNonStreamValuesEnd) remain declared for completeness; no orchestrator or public surface is added. The harness extension is preserved as a deterministic reproducer for the blocker: re-run --write-revision-values to verify the gate any time. docs/plans/revision-write-path.md updated with the empirical finding plus the original plan retained as historical context. 177/177 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f4709ff143 |
Speculative-items sweep: IntegralDivisor, cert tests, D3/D1/D2 findings
Plan: docs/plans/speculative-items-sweep.md (also covers parallelism + findings). Implemented: - C3: HistorianTagDefinition.IntegralDivisor (default 1.0). Wire bytes flip per the captured native serializer; live probe shows the server stores IntegralDivisor on EngineeringUnit (shared) rather than per-tag, so the value is accepted on the wire but doesn't visibly persist for the test EU. Documented in the property's doc-comment. - E: HistorianWcfCertOptionTests (5 tests) covering AllowUntrustedServer- Certificate validator installation + ServerDnsIdentity propagation through CreateEndpointAddress and CreateBindingPair. Investigated + documented (deferred): - D3: Discrete/String/Int1/Int8/UInt8 EnsT2 root cause — server-side ValidationFailed: "Transaction validation failed". Native AddTag's validator rejects non-analog types; not a wire-format issue. To unlock, need to capture a working native flow via a different code path (likely SMC's tag-import path or AddTagExtendedProperties carrying type-specific metadata). Defer until a customer asks. - D1: AddTagExtendedProperties feasibility — managed surface confirmed (ArchestrA.HistorianAccess.AddTagExtendedProperties + WCF op AddTagExtendedPropertyGroups). Cost estimated at 1-2 days of focused RE work due to CTagExtendedPropertyGroup payload complexity. Defer. - D2: AddRevisionValuesBegin/Value/End — sub-plan written at docs/plans/revision-write-path.md with 5-step capture sequence, workstream estimates, and risk register. Implementation deferred. 177/177 tests pass (was 172; +5 cert tests + 1 IntegralDivisor unit test, harness probe results not committed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5ce62a5900 |
Wire ApplyScaling, StorageRate; close out write-commands plan
ApplyScaling (HistorianTagDefinition.ApplyScaling): The EnsT2 trailer's second byte controls server-side scaling — `FE 00` mirrors MinRaw to MinEU and sets AnalogTag.Scaling=0; `FE 01` persists distinct MinRaw/MaxRaw and sets Scaling=1. Decoded by toggling set_ApplyScaling on the native harness and capturing the wire bytes for both values with identical inputs. The earlier docs claimed EnsureTagAsync needed a follow-up "UpdateTags" call; the WCF surface has no such operation — toggling that one byte is the whole fix. StorageRate (HistorianTagDefinition.StorageRateMs): Serializer accepts a non-default rate, validated empirically against the live server which only accepts quantized values (1000/5000/10000/60000/300000 ms). EnsureTagAsync upsert semantics: Second call on the same tag name with different fields succeeds and updates Description, MinEU, MaxEU, MinRaw, MaxRaw, Scaling in place (verified by direct SQL inspection in a live test). Plan + doc closeout: write-commands-reverse-engineering.md rewritten as a current-state plan with three workstreams (A doc closeout / B idempotency / C1 StorageRate) and a parallelism table; prior phase notes preserved as appendix. handoff.md, implementation-status.md, wcf-contract-evidence.md, README.md updated to remove "writes are out of scope" / non-existent UpdateTags references and document the actual EnsT2 wire format including the `FE xx` trailer. Reverse-engineering harness gains --write-apply-scaling and a SQL post-check that prints the persisted AnalogTag bounds so future RE sessions can verify wire→DB causality without leaving the harness. 169/169 tests pass (was 165; +4 new tests covering ApplyScaling, StorageRate golden bytes, StorageRate live persistence, and EnsureTagAsync upsert semantics). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
200493c990 |
DelT investigation: wire-byte parity is necessary but not sufficient
Investigation step 1 — wire-byte parity check. Captured native DelT sends ref input values statusSize=1 + status=null (encoded as .nil on the wire). SDK was passing statusSize=0 + status=[] (empty array). Updated SDK to match native input values. Investigation step 2 — verified DelT still doesn't work standalone. With the ref-input fix, SDK DelT now returns false (instead of the previous true-with-no-effect). Tag continues to persist in Runtime.dbo.Tag. So the wire-byte parity fix moved the symptom but didn't resolve the root cause. Investigation step 3 — discovered EnsureTagAsync is ALSO silently broken. Byte-for-byte wire matches captured native EnsT2 (golden test still passes), but the call returns false and does NOT create the tag in the DB. The earlier "EnsureTagAsync round-trip test passing" was relying on the persistent tag from the broken DelT — a false positive. Two distinct issues remain: 1. EnsT2 silently fails server-side (returns false; no tag created) 2. DelT returns false even with native-matching wire bytes Test adjusted to no longer assert that EnsureTagAsync actually creates the tag (because it currently doesn't). Test still exercises the SDK call path to confirm it doesn't throw. Next-session diagnostic: write a custom IClientMessageInspector for the SDK's WCF channel that captures outgoing DelT/EnsT2 bytes to a file. Compare byte-for-byte (offset by offset, not just per-field) against captured native to isolate the difference. 130/130 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cfc8d44e3a |
Implement EnsureTagAsync (live-verified) + DeleteTagAsync (DelT semantics partial)
New SDK surface:
HistorianClient.EnsureTagAsync(HistorianTagDefinition)
HistorianClient.DeleteTagAsync(string tagName)
Plumbing:
src/AVEVA.Historian.Client/Models/HistorianTagDefinition.cs
Public input model — TagName/Description/EngineeringUnit/DataType/MinEU/MaxEU.
Currently only HistorianDataType.Float is live-verified.
src/AVEVA.Historian.Client/Wcf/HistorianTagWriteProtocol.cs
SerializeAnalogCTagMetadata produces 146-byte payload byte-for-byte
identical to the captured native EnsT2(Float) request.
SerializeDeleteTagNames produces ushort 0x6751 + ushort 1 + uint count
+ per-tag (uint charCount + UTF-16 chars).
src/AVEVA.Historian.Client/Wcf/HistorianWcfTagWriteOrchestrator.cs
Both EnsT2 and DelT run the full Stat-priming chain captured for the
analog flow (UpdC3 + Stat.GetV ×3 + Stat.GETHI ×2 + 7× GetSystemParameter
+ Trx.GetV + Retr.GetV).
src/AVEVA.Historian.Client/Wcf/HistorianWcfTagClient.cs
MapDataType extended to accept tag-origin marker 0xC7 (SDK-created tags).
Tests:
5 golden-byte tests (HistorianTagWriteProtocolTests):
SerializeAnalogCTagMetadata byte-for-byte match against captured 146-byte fixture
SerializeAnalogCTagMetadata produces different bytes for different inputs
SerializeDeleteTagNames single-tag matches captured shape
SerializeDeleteTagNames multi-tag appends each
SerializeDeleteTagNames empty list throws
1 live integration test (gated by HISTORIAN_WRITE_SANDBOX_TAG):
EnsureTagAsync_AndDeleteTagAsync_RoundTrip_AgainstLocalHistorian
EnsureTagAsync creates the sandbox tag, GetTagMetadataAsync reads it
back. 130/130 tests pass.
Harness improvements:
--write-delete-after now runs DelT independently of AddStreamedValue
outcome.
HistorianTagStatusList constructed correctly for DeleteTags reflection
call (previous StringCollection attempt failed with TypeMismatch).
Known DelT gap: SDK's DeleteTagAsync returns true but server-side
cascading deletion does not always complete (the row remains in
Runtime.dbo.Tag). The captured native flow's DelT removes the tag
cleanly (verified via harness --write-delete-after), so something
around the WCF DelT call is missing from our orchestrator. Documented
as known issue with SMC-based cleanup as workaround.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
b3d22befd0 |
write-commands plan: AddS2 prereq is architectural - not implementable as generic client write
Three follow-up attempts to satisfy the AddS2 server-cache prereq all
failed at the same client-side gate before any AddS2 byte reached the
wire:
1. TagKey synthetic→real override. First attempt used the placeholder
TagKey=10000000 returned by HistorianAccess.AddTag. Native
AddStreamedValue refused with error 168 "Tag not added to server".
Harness now ALWAYS resolves the real wwTagKey from Runtime.dbo.Tag
after AddTag (logged as TagKeyOverride: Synthetic→RealFromSql).
Error code shifted to 129 "Tag not found in cache" — request now
reaches the server but the server's in-memory tag cache doesn't
know about the new tag.
2. Server-cache settle wait. Up to 8s sleep between AddTag and
AddStreamedValue (--write-resync-wait-seconds N). Wait period
contains 2× UpdC3 + 2× Trx/GetV keep-alives but no server-side
cache update — error 129 persists.
3. Fresh process / fresh connection. Skipped AddTag entirely
(--write-skip-add-tag) and ran AddStreamedValue alone against the
already-existing sandbox tag. New native client instance, new
client-side cache, new server session. SAME error 129 — no AddS2
bytes sent on wire. Capture confirms 44 records ending in Close2.
Interpretation: the Historian engine's runtime tag cache only ingests
tags from configured IOServers / Application Server data pipelines,
not from HistorianAccess.AddTag-only client flows. AddTag populates
Runtime.dbo.Tag (wwTagKey=240 was created) but doesn't register the
tag with the live cache that AddStreamedValue checks. That
registration happens server-side when an upstream data producer (an
OPC driver, AnE event subsystem, Application Server attribute store)
claims the tag.
WriteValueAsync therefore CANNOT be implemented as a generic client
API against this server architecture. The SDK's realistic writeable
surface is now narrowed to EnsureTagAsync + DeleteTagAsync only.
Harness changes:
- --write-skip-add-tag skip the AddTag call (for fresh-cache test)
- --write-skip-add-value skip the AddStreamedValue call (capture EnsT2 only)
- --write-resync-wait-seconds N sleep N seconds between AddTag and
AddStreamedValue (default 0)
- TagKey lookup now ALWAYS hits SQL after AddTag, not just when
the synthetic key is 0.
Plan doc updated with full Phase 2 follow-on findings + revised
remaining work (4-item checklist focused on EnsureTagAsync/
DeleteTagAsync, plus a stretch goal of probing AddRevisionValues*
against an existing-tag).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
e3c003d978 |
write-commands plan: execute Phase 1 discovery (no DB writes)
Phase 1 of docs/plans/write-commands-reverse-engineering.md is purely
discovery — static IL inspection, public-API enumeration, scope
elimination — and produces no DB writes. Phase 2 requires extending
NativeTraceHarness with a write scenario AND explicit operator approval
per the plan's safety rule §1, so it is deferred to a separate session.
Phase 1 findings recorded inline in the plan's status header:
1. §3.4 ModifyData/DeleteData eliminated — no managed wrapper exists.
`methods` returns zero hits for EditValue, ModifyValue, EditData,
DeleteData, ModifyData, OverwriteData. Per the plan's own §3.4
disposition rule, this op is REST/SMC-only.
2. §4.a native serializer tokens identified for Phase 2:
Public managed-wrapper write API in ArchestrA.HistorianAccess:
- AddTag (0x0600619A)
- AddStreamedValue × 3 overloads (0x0600618C/D/E)
- AddNonStreamedValue × 2 overloads (0x0600618F/90)
- DeleteTags (0x060061A4)
- AddRevisionValuesBegin / AddRevisionValue / AddRevisionValuesEnd
/ AddRevisionValues (0x06006175-77, 0x0600617F)
— covers the bulk-modify use case the eliminated ModifyData would
have served. Worth folding into Phase 2 scope.
Native serializers:
- CTagUtil.ConvertTagMetadataToHistorianTag (0x060055CE) — 412 IL
instructions; builds CTagMetadata for analog/discrete/string.
Calls every CTagMetadata accessor we'd need to surface
(Min/MaxRaw, Min/MaxEU, Unit, Message0/1, MaxLength,
IntegralDivisor, DefaultTagRate, RolloverValue) plus all the
CDataType predicates already decoded.
- CHistoryConnectionWCF.AddStreamValuesToHistorian (0x0600404C)
— confirms the on-wire shape matches our existing
IHistoryServiceContract2.AddStreamValues2 declaration.
3. Chicken-and-egg resolved: the first §3.1 EnsT2 test creates the
sandbox tag itself, so no SMC step is needed.
4. Open question §8.6 answered — the wrapper exposes both
AddStreamedValue and AddNonStreamedValue, so the SDK should
eventually surface both real-time and backfill write modes.
Phase 2 next steps recorded inline in the plan as a 5-item executable
checklist (extend harness with --scenario write, capture, decode
EnsT2(analog) + AddS2 bytes, implement public surface, gated live
tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6888b8c55a |
Wire SDK for remote-TCP end to end; live-verify RemoteTcpIntegrated
Executes docs/plans/tcp-connection-validation.md. Full read-only SDK
surface now works against a remote AVEVA Historian over Net.TCP with
Windows transport authentication. 124/124 tests pass; the +10 new live
integration tests in RemoteTcpIntegrationTests.cs are gated by
HISTORIAN_REMOTE_TCP_HOST + HISTORIAN_REMOTE_TCP_TAG.
Two SDK bugs found while executing the plan:
1. Historian2020ProtocolDialect.ReadRawAsync / ReadAggregateAsync /
ReadAtTimeAsync / ReadEventsAsync had explicit
`if (_options.Transport != HistorianTransport.LocalPipe) return Missing<T>`
guards. These were a guardrail from before the orchestrators handled
TCP; the orchestrators have always used CreateBindingPair(options)
which dispatches on transport correctly. Gates removed.
2. HistorianWcfStatusClient and HistorianWcfEventOrchestrator hardcoded
HistorianWcfBindingFactory.CreatePipeEndpointAddress for the auxiliary
services (Stat, Trx, Retr). Worked for LocalPipe; for TCP it produced
an EndpointAddress with scheme net.pipe attached to a TCP binding
(channel factory rejected the URI). Worse, when only the endpoint was
transport-aware, the binding still requested a Windows-transport-
security upgrade that the Stat endpoint over TCP doesn't support
(auxiliaries don't repeat the auth — the Hist session is already
authenticated). Added two helpers:
- HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(options, name)
-> net.pipe for LocalPipe, net.tcp for remote
- HistorianWcfBindingFactory.CreateAuxiliaryBinding(options)
-> NamedPipe for LocalPipe, plain MdasNetTcpBinding for remote
Both call sites updated.
Live verification against the remote (probed previously in prior
sessions; reachability re-confirmed today):
- ProbeAsync over RemoteTcpIntegrated and RemoteTcpCertificate
- ReadRawAsync (8 samples returned for SysTimeSec)
- ReadAggregateAsync (TimeWeightedAverage, 1-min cycle, 10-min window)
- ReadAtTimeAsync (3 timestamps)
- BrowseTagNamesAsync (finds the test tag)
- GetTagMetadataAsync (full metadata populated)
- ReadEventsAsync (chain runs without throwing)
- GetConnectionStatusAsync (ConnectedToServer=true)
- GetSystemParameterAsync (HistorianVersion="20,0,000,000")
The default 'NT SERVICE\aahClientAccessPoint' SPN turned out to work
for the remote too — discovery workstream A (SPN-finding) was not
needed in practice.
README and the TCP plan doc updated to reflect the executed status.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
1b31c24c8d |
Plan TCP connection validation (live verification of the existing remote-TCP plumbing)
docs/plans/tcp-connection-validation.md (308 lines): Plan to live-verify the RemoteTcpIntegrated and RemoteTcpCertificate transports against an actual remote AVEVA Historian. The SDK's HistorianWcfBindingFactory already builds all three bindings (CreateMdasNetTcpBinding, CreateMdasNetTcpWindowsBinding, CreateMdasNetTcpCertificateBinding) but only LocalPipe has been exercised end-to-end. Wire format is identical across transports; only WCF binding shape and credential negotiation differ. Discovery workstreams A/B/C run in parallel (SPN discovery via static IL + WCF probe; cert binding requirements via wcf-cert-probe; operator preconditions checklist). D blocks on A. Verification tracks V1-V5 also parallelize once V1 (ProbeAsync) confirms the transport is reachable. Includes risks (SPN mismatch, cert chain validation, idle disconnect, Open2 response delta, compression negotiation, time skew, false-positive empty reads), success criteria, eight open questions, and explicit out-of-scope items filed under the existing write-commands and store-forward plans. No code changes; no preconditions assumed met. Implementer must satisfy §2 preconditions (reachable remote Historian, port 32568 open, test account, SPN registered, etc.) before §4 discovery starts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |