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
This commit is contained in:
@@ -257,12 +257,51 @@ StartDateTime and EndDateTime"`.
|
||||
|
||||
---
|
||||
|
||||
## Event-send wire format ✅ (captured 2026-06-20)
|
||||
|
||||
`AddStreamedValue(HistorianEvent)` leaves the client over **WCF as `AddS2`
|
||||
(`IHistoryServiceContract2.AddStreamValues2(string handle, byte[] pBuf, out errorBuffer)`)** —
|
||||
**not** the storage-engine pipe. (Captured with the NativeTraceHarness `event-send` scenario +
|
||||
instrument-wcf-writemessage; two events diffed to separate constant framing from value fields.)
|
||||
CM_EVENT is a built-in registered tag, so the `129 TagNotFoundInCache` gate that blocks `AddS2`
|
||||
for user tags doesn't apply. Open2 uses event connection-mode **0x501** (vs 0x402 read / 0x401
|
||||
write). The `pBuf` is a storage **sample buffer** wrapping the event VTQ:
|
||||
|
||||
```
|
||||
0x00 UInt16 0x534F "OS"
|
||||
0x02 UInt16 sampleCount = 1
|
||||
0x04 UInt32 packet length = delivered byte[].Length (= valueBlob.Length + 11)
|
||||
0x08 UInt16 valueBlob.Length + 1
|
||||
0x0A valueBlob:
|
||||
+0x00 GUID CM_EVENT tag id (353b8145-5df0-4d46-a253-871aef49b321)
|
||||
+0x10 Int64 EventTime FILETIME UTC (floored to ms — the VTQ timestamp)
|
||||
+0x18 UInt16 OpcQuality = 192
|
||||
+0x1A UInt16 192
|
||||
+0x1C UInt16 0x118D (opaque CCommonArchestraEventValue descriptor — constant)
|
||||
+0x1E GUID event Id
|
||||
+0x2E Int64 ReceivedTime FILETIME UTC (full 100ns; native uses a unique/monotonic time)
|
||||
+0x36 compact-ASCII string Namespace
|
||||
+.... compact-ASCII string EventType
|
||||
+.... UInt16 eventStructVersion = 5
|
||||
+.... UInt16 propertyCount
|
||||
+.... propertyCount × { compact-ASCII name; value: UInt8 typeMarker, UInt8 len, UInt8 status, len×bytes }
|
||||
trailing: 1 pad byte so byte[].Length == packet length (UInt32 @0x04). The native relies on
|
||||
the MDAS encoder adding this byte (non-deterministic value); the SDK emits 0x00.
|
||||
```
|
||||
|
||||
Property values reuse `HistorianEventRowProtocol`'s typed encoding; only `0x43` (UTF-16 string)
|
||||
is captured on the write side so far. Implemented in `HistorianEventWriteProtocol` (golden-tested)
|
||||
and shipped as `HistorianClient.SendEventAsync`. **Server accepts the `AddS2` (success, empty
|
||||
error buffer); on-box persistence is environment-gated (the native client behaves identically).**
|
||||
|
||||
## Open threads
|
||||
|
||||
- 🔶 Event value blob: field set/order known (§2a); **exact native framing** still needs one
|
||||
`CCommonArchestraEventValue::PackToVtq` output capture + golden-byte test (mirror the read-side
|
||||
`HistorianEventRowProtocol` reverse-engineering). Now feasible locally — the live historian is
|
||||
installed, so the same instrument-and-capture approach used for reads applies.
|
||||
- ⚠️ Event **persistence**: accepted `AddS2` events do not land in `v_AlarmEventHistory2` on the
|
||||
local dev box (native client identical) — the event storage/ingestion pipeline isn't active
|
||||
here. Needs a Historian with active event storage to verify send→store→read-back end-to-end.
|
||||
- 🔶 Non-string event property write encodings (bool/int/filetime/guid/double) and revision/
|
||||
update/delete event sends (RevisionVersion≠0, IsUpdate/IsDelete) are **not captured**; the SDK
|
||||
throws `ProtocolEvidenceMissingException` for them. One capture each to extend.
|
||||
- ❓ `EnqueueEventDataPacket.SerializedBytes` packet framing (header + N event VTQs batched).
|
||||
- ✅ Database-mode store: server writer is `aahEventStorage.exe` loading the managed
|
||||
`ArchestrAEvents.EventStorage.Contract` connection assembly; SQL retrieval surface is the
|
||||
|
||||
Reference in New Issue
Block a user