4fcda87ecd5852cc7ef00f55d42878280dc01dc6
29 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4fcda87ecd |
root/docs: index mbproxy's post-Phase-9/10/11 capabilities
Multiplexing, read coalescing, and opt-in response caching change what someone might come to mbproxy for, so the root index now names them alongside BCD rewriting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1db900edef |
mbproxy: add opt-in response cache (Phase 11)
Layers a per-PLC, per-tag response cache on top of Phase 10's coalescing.
Cache is OFF by default per tag (CacheTtlMs = 0); a fresh deployment with no
TTL config behaves identically to Phase 10. Operators opt tags in by setting
CacheTtlMs > 0 on a BcdTagOptions entry (or DefaultCacheTtlMs > 0 on a
PlcOptions entry), explicitly acknowledging the staleness window.
Cache lookup order: cache -> coalesce -> backend. A cache hit short-circuits
both Phase 10's coalescing path and Phase 9's backend send. Cache stores
POST-rewriter PDU bytes so hits never re-invoke the BCD rewriter. FC06/FC16
write responses invalidate every cached entry whose address range overlaps
the write (half-open interval math).
New types (Mbproxy.Proxy.Cache, all internal):
- CacheKey (record-struct, same shape as CoalescingKey but kept SEPARATE so
the two phases evolve independently).
- CacheEntry, ResponseCache (IDisposable; LRU + PeriodicTimer eviction
loop), CacheInvalidator (pure overlap matcher), CacheLogEvents (stable
mbproxy.cache.* names).
Multi-tag range TTL = min(TTLs); any tag with TTL = 0 in the range disables
caching for the whole read (conservative-by-design).
Options surface:
- BcdTagOptions.CacheTtlMs (nullable int; null = fall through to PLC default)
- PlcOptions.DefaultCacheTtlMs
- MbproxyOptions.Cache.{AllowLongTtl, MaxEntriesPerPlc, EvictionIntervalMs}
- TTL > 60_000 ms requires Cache.AllowLongTtl = true (reload validation).
Admin counters (Tier 1.8 + Tier 2 cache-memory KPIs from docs/kpi.md):
- CacheHitCount, CacheMissCount, CacheInvalidations on ProxyCounters.
- CacheEntryCount, CacheBytes via a new ICacheStatsProvider snapshot path.
- /status.json and the HTML page surface a new Cache cell per PLC row.
Hot-reload: any tag-list change to a PLC reseats the per-PLC context with a
fresh cache; the old cache is disposed inside ReplaceContextAsync. Per-tag
flush granularity is intentionally not implemented in v1.
PLCs with no cache-eligible tags (every resolved tag has CacheTtlMs = 0)
get Cache = null on the context and skip the eviction timer entirely, so
the no-cache path is byte-identical to Phase 10.
Tests (32 new unit + 5 new E2E = 37 new; suite now 314 unit + 48 E2E):
- CacheKeyTests, CacheEntryTests (records + boundary semantics).
- CacheInvalidatorTests: full overlap, both partials, adjacent-not-
overlapping, disjoint, different unit ID + auxiliary FC-filter / zero-qty.
- ResponseCacheTests: round-trip, lazy expiry, range invalidation,
unit-id filter, LRU bound, LRU access tracking, concurrent get/set,
dispose, clear, approximate-bytes accounting.
- ResponseCacheMultiplexerTests (stub-backend): hit short-circuits
coalescing, BCD-decoded bytes are cached not raw, FC06 invalidates
overlapping, non-overlapping write does not invalidate, multi-tag
TTL=min rule, regression-cache-disabled-by-default-is-Phase-10, hit
works even when backend unreachable.
- ResponseCacheE2ETests (pymodbus DL205 sim, sequential reads):
* Headline: 10 reads with TTL=1000 ms -> 9 hits, 1 miss, 1 backend trip.
* TTL expiry path with sleep > TTL.
* Write invalidation through the proxy on a scratch register.
* BCD-decoded bytes are cached, not raw BCD nibbles.
* Regression: Cache disabled by default -> behaviour byte-identical to
Phase 10.
Pre-existing flake hardened: BackendDisconnect_CascadesToAllUpstreams now
polls briefly for the cascade counter to absorb the inherent scheduling
gap between "upstream EOF observed" and "counter incremented inside
TearDownBackendAsync." Counter semantics unchanged.
Phase doc updated with implementation clarifications discovered during
this work (CacheKey kept separate from CoalescingKey, LastUsedTick is
long, FC06/FC16 startAddr/qty parsing extension, cache-pre-connect
short-circuit, write-invalidation only on successful responses).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
892b10baf4 |
mbproxy/docs: pivot design contract for Phase 11 response cache
Lands the design-contract pivot ahead of any cache implementation code so reviewers can evaluate the change to the "purely transparent proxy" stance independently of the Phase-11 code that depends on it. - docs/design.md: rewrite "What this is" / Read-coalescing / Failure-modes sections to acknowledge the opt-in cache; add new "Response cache (Phase 11)" section covering lookup order (cache -> coalesce -> backend), multi- tag range TTL = min, post-rewriter storage, address-range-overlap write invalidation, hot-reload PLC-wide flush, no-persistence, AllowLongTtl gate, and LRU-bounded capacity. Extend log event table with mbproxy.cache.* events. Extend per-PLC status field table with cacheHitCount / cacheMissCount / cacheInvalidations / cacheEntryCount / cacheBytes. Extend hot-reload propagation table with CacheTtlMs / Cache.* rows. - docs/kpi.md: graduate Tier 1.8 (response cache) from "requires Phase 11" to "shipped in Phase 11" and add Tier 2.4a cache-memory section. - CLAUDE.md (mbproxy): update Purpose paragraph and the Architecture headline bullets to reflect the transparent-by-default + opt-in-cache contract; flip "Implementation complete through Phase 10" to "through Phase 11". - install/mbproxy.config.template.json: add a fully-commented Mbproxy.Cache block and a CacheTtlMs example on a BcdTags.Global entry, with prominent staleness commentary documenting the design contract. No code changes in this commit - implementation lands in a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a2dba4bd07 |
mbproxy: add in-flight read coalescing (Phase 10)
When two or more upstream clients send the same FC03/FC04 read while a matching request is already in flight on the same PLC's multiplexed backend socket, attach the late arrivals to the existing InFlightRequest .InterestedParties list instead of opening a second backend round-trip. The single backend response fans out to every attached party with each party's original MBAP TxId restored individually. Zero post-response staleness — coalescing operates entirely within the in-flight window (microseconds to ~10 ms typical); the proxy is NOT a cache layer. Headline mechanism: - New record struct CoalescingKey(UnitId, Fc, StartAddress, Qty) keys the per-PLC InFlightByKeyMap. FC03 and FC04 are separate Modbus tables and never share a key; different unit IDs never coalesce; writes (FC06/FC16) bypass the coalescing path entirely. - InFlightByKeyMap uses a simple lock around a Dictionary; atomic TryAttachOrCreate either appends a new party to the in-flight request's mutable List<InterestedParty> or invokes a factory to build a fresh entry. Per-entry MaxParties cap (default 32) bounds fan-out cost; past the cap, the next arrival opens a new entry. - PlcMultiplexer.OnUpstreamFrameAsync takes the coalescing path for FC03/FC04 when Mbproxy.Resilience.ReadCoalescing.Enabled. The factory closure does the Phase-9 work (allocate TxId, add to CorrelationMap); the channel send happens AFTER returning from TryAttachOrCreate so the map lock is not held across the async send. - Response fan-out in RunBackendReaderAsync removes the entry from InFlightByKeyMap before iterating InterestedParties, ensuring no concurrent attach can mutate the list during iteration. - Cascade + watchdog paths also drain the key map so a stale entry cannot outlive its backend round-trip. Counter accounting balance (per snapshot): CoalescedHitCount + CoalescedMissCount equals total FC03 + FC04 requests since startup. Even with coalescing disabled, every read still bumps Miss so dashboard math stays balanced. New surface (additive only): - src/Mbproxy/Proxy/Multiplexing/CoalescingKey.cs - src/Mbproxy/Proxy/Multiplexing/InFlightByKeyMap.cs - src/Mbproxy/Proxy/Multiplexing/CoalescingLogEvents.cs - ReadCoalescingOptions on ResilienceOptions - CoalescedHitCount / CoalescedMissCount / CoalescedResponseToDeadUpstream counters surfaced on /status.json per PLC and as a compact "Coal" cell on the HTML status page. Phase 9 test patch: TwoUpstreams_ProxyTxIds_AreDistinct_OnTheWire previously read the same register from both clients (which now coalesces). Patched to read two different addresses so the test still proves distinct backend TxIds without violating the coalescing contract. Tests added: 24 new (19 unit + 5 E2E): - CoalescingKeyTests (5) - InFlightByKeyMapTests (6, includes concurrent stress) - ReadCoalescingTests (8, stub-backend with deterministic delay) - ReadCoalescingE2ETests (5, pymodbus simulator; coalescing-active during overlap is proven against the stub, not the sim, due to pymodbus 3.13's known concurrent-frame bug) Total: 325 tests passing (282 unit + 43 E2E). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
56eee3c563 |
mbproxy: initial commit through Phase 9 (TxId multiplexing)
Adds the mbproxy service end-to-end. Phases 00-08 implement the production-ready single-listener / 1:1-backend transparent Modbus TCP proxy with bidirectional BCD rewriting for the ~54-PLC DL205/DL260 fleet. Phase 9 replaces the connection layer with a single backend socket per PLC plus MBAP TxId rewriting, lifting the H2-ECOM100's 4-concurrent-client cap as an operational ceiling. Phase 9 additions of note: - PlcMultiplexer + UpstreamPipe + TxIdAllocator + CorrelationMap - InFlightRequest with IReadOnlyList<InterestedParty> (load-bearing for Phase 10 read coalescing — do not collapse to a single field) - Per-request watchdog: surfaces Modbus exception 0x0B to upstream on BackendRequestTimeoutMs, defending against lost responses, dead-PLC paths, and pymodbus 3.13.0's concurrent-multiplexed- request bug (its ServerRequestHandler.last_pdu state race) - Status DTO + HTML gain inFlight / maxInFlight / txIdWraps / disconnectCascades / queueDepth (Tier 1.6 in docs/kpi.md) Tests: 263 unit + 38 E2E. Multiplexer correctness under truly concurrent backend traffic is proved against a stub backend in PlcMultiplexerTests; MultiplexerE2ETests paces requests so pymodbus 3.13's single-PDU framer stays in known-good mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2e937228a0 |
mxaccesscli: add read-batch / write-batch / subscribe-batch (JSONL input)
Three new subcommands that take a JSONL file (or '-' for stdin) and reuse a
single MxSession across all entries. The big win is in write-batch: a
two-phase pipeline (Advise all -> drain DataChange to resolve types; Write
all -> drain WriteComplete) reduces wall time from N x (resolve + write_ack)
to ~max(resolve) + ~max(write_ack). Measured 38.2s -> 10.3s (~3.7x) for
four writes against the ZB dev galaxy; the saving grows with N.
Per-item continue-on-error: parse errors are collected line-by-line and
abort with exit 2 before any LMX session opens; runtime failures (resolve
timeout, bad references, coerce errors, write timeouts) get their own
results[] row with a typed `error` string and exit 1. Auth flags mirror
`mxa write` and are resolved once before Phase A.
Shared infra:
- Mx/JsonlInputReader.cs: lazy line reader (skips blank / '#' lines),
bare-string or {"tag":"..."} for read/sub, {"tag","value","type"?} for
write, with array-suffix consistency check at parse time.
- Mx/ValueCoercion.cs: new CoerceJToken(...) wrapper preserves the
single source of truth for type vocabulary.
Docs:
- README run examples extended for each new command.
- docs/usage.md: new "Batch input format" subsection (shared contract),
one section per command with envelope examples and a full
failure-mode table for write-batch, plus a "Batch commands -
verified live" section capturing the 2026-05-10 ZB-galaxy run and
pipelining-timing numbers.
- test-fixtures/ holds the exact JSONL files used in the verified-live
run so the doc numbers are reproducible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6cde4d9fe4 |
graccesscli: scripts-get now uses direct read; bypass anon-type reflection
Two follow-on fixes after the MxValueDetails reader correction:
1. AttributeValueDetails was falling through to the package-binary
fallback even when MxValueDetails returned Supported=true. Cause:
`ReadString(direct, "Supported")` reflects on an anonymous type via
InvokeMember, which returned an empty string under
`BindingFlags.IgnoreCase`, making the StringEquals check fail.
Replace with a typed `Type.GetProperty("Supported").GetValue(direct)`
bool check via the new IsDirectSupported helper.
2. ObjectScript (the `scripts get` body retrieval) skipped the live
attribute path entirely and went straight to the package-binary
parser. The parser truncates strings at certain delimiters (e.g.
marker text containing `\n` or `=` came back as just the prefix).
Now ObjectScript tries `obj.ConfigurableAttributes[<script>.<field>]
.value.GetString()` first and only falls back to the package body
when the direct read returns null. Matches the pattern in
AttributeValueDetails. The package fallback survives for cases
where direct GRAccess doesn't surface the attribute.
End-to-end live round-trip on $DelmiaReceiver.ProcessRecipe.ExecuteText
with marker `// scripts-get round-trip marker\nMe.RecipeDownloadFlag = false;`:
scripts get -> Source: direct-graccess
Body: <marker>
attribute value get -> Value: <marker>
Both readers return the same content the writer wrote. Field then
restored to the original `Me.RecipeDownloadFlag = false;`.
Tests still 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
842b94fb39 |
graccesscli: fix attribute value reader (typed IMxValue + IgnoreCase)
The MxValueDetails reader was the root of the false "writeback gap"
diagnosis. Three problems compounded:
1. ReadProperty(attr, "Value") used case-sensitive late-binding via
InvokeMember. The COM-side property is exposed as `value` (lowercase)
on IAttribute, so the lookup silently returned null.
2. Even if the MxValue was retrieved, the accessor probe loop
(Invoke(value, "GetBoolean") -> "GetInteger" -> ... -> "GetString")
relied on IDispatch late-binding. The MxValue interface's accessors
are vtable-only — they aren't reachable through IDispatch — so every
call threw, and the reader reported "no supported scalar accessor
succeeded" even when the typed accessor would have returned the
value cleanly.
3. The probe loop ordering put GetString last, so for any value that
somehow survived the type errors silently, an earlier accessor could
have returned a wrong-typed result.
Fix:
- Add `BindingFlags.IgnoreCase` to ReadProperty + Invoke. COM IDispatch
is case-insensitive at the IDL level; .NET InvokeMember is not unless
explicitly told to be. Comment explains the trap.
- AttributeValueDetails now uses typed `attr.value` (and falls back to
the same lookup via obj.ConfigurableAttributes when the runtime proxy
returns null), bypassing the late-binding gap entirely.
- MxValueDetails casts to IMxValue and dispatches on
GetDataType().ToString(), calling the typed scalar accessor for the
declared type (GetBoolean / GetInteger / GetFloat / GetDouble /
GetString / GetElapsedTime). MxBigString and MxInternationalizedString
share the GetString path. MxTime is left as a follow-up; its
VBFILETIME field layout differs across interop builds.
- Replaced the misleading "Attribute value is not exposed" / "no
scalar accessor succeeded" messages with specific diagnostics that
identify the actual failure (wrong type cast, typed accessor exception,
unhandled MxDataType).
Live verification on $DelmiaReceiver.ProcessRecipe.DeclarationsText:
BEFORE fix: `attribute value get` → Supported: false, Value: null,
"Attribute value is not exposed by this GRAccess attribute."
AFTER fix: `attribute value get` → Supported: true, DataType: MxString,
Value: "// roundtrip-test marker (graccesscli scripts set
--field DeclarationsText)\n"
The reader now correctly surfaces the value graccesscli's writes have
been persisting all along. This is the gap that produced the "writeback
gap" mirage in the previous round of investigation.
Tests still 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c52d8d0171 |
graccesscli: correct script-edit docs to TN-537 truth (writes DO persist)
I was wrong. AVEVA Tech Note 537 ("Creating an Application Object Script
Using GRAccess", April 2008) documents the supported pattern:
ConfigurableAttributes[<script>.<field>].SetValue(MxValue) inside a
CheckOut/Save/CheckIn cycle. graccesscli's existing
FindAttributeForMutation already follows this — writes to MxCategoryPackageOnly_Lockable
script-text fields persist correctly.
The earlier "writeback gap" diagnosis was a phantom caused by a reader-side
issue. `object attribute value get` against a script body returns
"Supported: False / Attribute value is not exposed" because
MxValueDetails uses a case-sensitive `ReadProperty(attr, "Value")` lookup
plus an accessor probe (GetBoolean -> GetInteger -> GetFloat -> GetDouble
-> GetString) that can fall through silently for some MxValue shapes. The
COM-side property is exposed as `value` (lowercase), readable as
`attr.value.GetString()` -- which the live probe at
`analysis/ide-edit-investigation/probe_setvalue/` does and confirms the
post-write content matches the marker exactly.
Live verification on $TestMachine.UpdateTestChangingInt.DeclarationsText
and $DelmiaReceiver.ProcessRecipe.{ExecuteText,DeclarationsText}:
=== verdict ===
marker landed on same-proxy ConfigurableAttributes: True
marker landed on same-proxy Attributes : True
marker landed on fresh-proxy ConfigurableAttributes: True
marker landed on fresh-proxy Attributes : True
The probe also confirmed that two earlier graccesscli `object scripts set`
invocations (which I had wrongly believed failed) had persisted -- the
marker text I wrote previously was still on disk in
ProcessRecipe.{ExecuteText,DeclarationsText} when read directly via
attr.value.GetString(). The probe restored both fields to their original
values.
This commit:
- Updates the misleading [Command(...)] / [CommandOption(...)]
descriptions in GRAccessSurfaceCommands.cs back to honest versions
citing TN-537.
- Restores the --file-using examples for `object scripts set` and
`object scripts create` across script-editing.md, llm-integration.md,
usage.md, and zb-testmachine.md.
- Removes the test that asserted the (wrong) EnsureMutableViaSetValue
guard. Re-aims ScriptCommandDescriptions_… at the corrected wording.
- Removes two leftover EnsureMutableViaSetValue calls in the trigger-period
/ trigger-type write paths (both targeted MxCategoryWriteable_C_Lockable
attributes; would never have fired even if the helper still existed).
- Adds analysis/ide-edit-investigation/REPORT.md (replacing the earlier
wrong report) plus the probe sources under probe_setvalue/.
The MxValueDetails reader gap (case-sensitive ReadProperty + accessor
probe) is a real follow-up: `object attribute value get` should
case-insensitively read `value` and try GetString first when the
underlying MxValue.DataType is MxString. Out of scope here -- that's a
separate, smaller fix.
Test count delta: 67 -> 66 (-2 wrong tests, +1 corrected description test).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e4e54254d8 |
Revert "graccesscli/docs: document the package-only writeback boundary"
This reverts commit
|
||
|
|
4e242ca824 |
Revert "graccesscli: fail fast on package-only attribute writes (writeback gap fix)"
This reverts commit
|
||
|
|
87c0124174 | Clarify script mutation limits | ||
|
|
c12fbc5988 |
graccesscli/docs: document the package-only writeback boundary
Adds a new "Editing Script Content" section to script-parsing.md that
explains the IAttribute.SetValue silent-no-op behavior on
MxCategoryPackageOnly* attributes, enumerates which ScriptExtension
fields fall on each side of the boundary (text fields = no, trigger
settings = yes), names the EnsureMutableViaSetValue safety check that
landed in
|
||
|
|
bd95ace1c5 |
graccesscli: fail fast on package-only attribute writes (writeback gap fix)
Investigation findings: IAttribute.SetValue silently no-ops for any attribute whose AttributeCategory starts with `MxCategoryPackageOnly`. The COM call returns Successful=true and the dispatcher reports OK, but a verify-read via direct IAttribute.GetValue confirms the value is unchanged. Discovered while validating the new --field flag round-trip on \$DelmiaReceiver.ProcessRecipe.ExecuteText: - BEFORE write: ExecuteText body = `Me.RecipeDownloadFlag = false;` - WRITE: scripts set --field ExecuteText --file marker.txt → OK - AFTER write: ExecuteText body = `Me.RecipeDownloadFlag = false;` (UNCHANGED — the marker prefix from marker.txt is absent) - ConfigurableAttributes shows `ProcessRecipe.ExecuteText` category = `MxCategoryPackageOnly_Lockable`. Same for DeclarationsText, StartupText, ShutdownText, OnScanText, OffScanText, Expression. - Per docs/script-parsing.md:8, "Object-level scripts ... may appear as attributes, extension attributes, or only inside exported object packages" — script-text fields are in the third bucket. Trigger settings (TriggerType, TriggerPeriod) are `MxCategoryWriteable_C_Lockable` and remain effective via SetValue — verified by `scripts settings set --trigger-period-ms 1000` returning OK on a clean checkout state. Add `EnsureMutableViaSetValue(IAttribute, name)` helper that throws InvalidOperationException with a clear remediation hint when called on a package-only attribute. Wired into three write sites: - `case "value-set"` (object attribute value set) - `ObjectScriptSet` (scripts set body writer) - `SetScriptSettings` Expression branch (the one trigger-related field that's package-only; TriggerType/TriggerPeriod stay unguarded) Validation against live ZB galaxy: - Before fix: `scripts set --field ExecuteText` returned `success: true` but didn't mutate the body. - After fix: same call returns `success: false` with `"Attribute 'ProcessRecipe.ExecuteText' has category 'MxCategoryPackageOnly_Lockable'. Package-only attributes silently no-op via IAttribute.SetValue and can only be edited through the package import/export round-trip..."` (exit code 1). - `scripts settings set --trigger-period-ms 1000` against the same script: still OK (TriggerPeriod is Writeable_C_Lockable). The real fix for editing script bodies is the package-rewrite path — export `.aaPKG`, modify the binary script-extension records, re-import via `galaxy.ImportObjects`. graccesscli already has the read half (`PackageSnapshotParser.Parse`); the write half is a separate followup. 61 → 63 → 63 (no test count delta this commit; the new helper is exercised end-to-end by the live validation above; unit tests for the failure path require a mock IAttribute proxy that's out of scope here). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3f6bfebd6d |
graccesscli: extend script editor surface — --field, --lock-trigger-type, scripts delete
Three additions to the script editor commands. Each one closes a real gap surfaced by the round-trip-test against \$DelmiaReceiver.ProcessRecipe. 1. `object scripts set --field <name>` — explicit text-field selection. Previously `scripts set` always wrote to <Name>.ExecuteText (via ScriptAttributeName's default). To rewrite DeclarationsText / StartupText / ShutdownText / OnScanText / OffScanText / Expression, callers had to pass the full attribute name as `--script Foo.StartupText`, which is brittle. The new `--field` flag accepts any of the seven canonical ScriptTextSuffixes and composes <script>.<field> directly. Validates against the suffix list so an unrecognised --field surfaces a friendly error rather than a downstream FindAttributeForMutation failure. Default behavior (no --field) is unchanged: ExecuteText. 2. `object scripts settings set --lock-trigger-type` — parallel to the existing --lock-trigger-period. After writing TriggerType the new flag calls SetLocked(MxLockedInMe), matching the lock pattern on the period field. Without it, --trigger-type writes the value but leaves the attribute unlocked. 3. `object scripts delete` — script-named alias for the existing extension-delete subcommand. Wraps obj.DeleteExtensionPrimitive( "ScriptExtension", scriptName) inside AtomicObjectEdit (checkout / save / checkin). Removes the burden of remembering the generic `--extension-type ScriptExtension --primitive <Name>` form. Test count delta: 61 -> 63 (+2 command-shape assertions for the new ObjectScriptsSetCommand and ObjectScriptsDeleteCommand). Live round-trip-test against \$DelmiaReceiver.ProcessRecipe: - `--field DeclarationsText` write composed `ProcessRecipe.DeclarationsText`, CheckOut/Save/CheckIn all returned OK. - `--field ExecuteText` round-trip same. - A subsequent re-read shows the original body, suggesting that IAttribute.SetValue silently no-ops for ScriptExtension text fields on this GRAccess version (or the package-export reader pulls from a different snapshot than the just-saved revision). This is upstream of the editor surface — the new flags route correctly to the same SetValue path that scripts set already used. Diagnosing the SetValue ineffectiveness for script-text fields is a separate followup that should look at IScriptExtension-specific COM interfaces (per docs/script-parsing.md:8 "Object-level scripts are less direct"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
65537570b8 |
graccesscli: enumerate ScriptExtension via canonical text-attribute suffixes
`object scripts list` previously walked obj.Attributes through a
substring heuristic (`IsScriptAttribute`) that matched any name
containing "script", "expression", "execute", "startup", "shutdown",
or "scan". Two failure modes:
- "deSCRIPTion" contains "script", so every `MoveIn*.Description` /
`MoveOut*.Description` attribute on `$MESReceiver` (and any other
object with `.Description` UDAs) was mis-emitted as a script.
- `ScanState` / `ScanStateCmd` (ordinary attribute UDAs) leaked in via
the "scan" keyword.
Net result on $MESReceiver: 25 entries, all false positives, zero
true ScriptExtensions.
The GRAccess COM API does not expose an `IScriptExtension` enumerable
(verified against ArchestrA.GRAccess.dll string table — only
`AddExtensionPrimitive` / `DeleteExtensionPrimitive` /
`RenameExtensionPrimitive` mutators exist). The only attribute-side
signal that an object carries a real ScriptExtension is the canonical
text-attribute suffix list documented in docs/script-parsing.md:
ExecuteText, DeclarationsText, StartupText, ShutdownText, OnScanText,
OffScanText, Expression.
Replace `IsScriptAttribute` with `ScriptExtensionPrefix(name)` that
detects exactly those suffixes, then group attributes by prefix:
- Each ScriptExtension yields one logical entry named by its prefix.
- The Fields[] list reports which text attributes are present.
- Sibling `<prefix>.TriggerType` / `<prefix>.TriggerPeriod` attributes
are surfaced as TriggerType / TriggerPeriod when present.
- ExtensionType is always "ScriptExtension".
Outer `ObjectScripts(IGalaxy, IgObject, PackageSnapshot)` updated to:
- Thread ExtensionType / Fields / TriggerType / TriggerPeriod through.
- Look up body content by prefix first, then fall back to
`<prefix>.ExecuteText` (the package's serialized bodies are keyed
by full attribute name).
- Mark every `<prefix>.<field>` as emitted so the package-fallback
branch doesn't re-emit each text field as its own entry.
Drop dead `IsScriptAttribute` (no other callers).
Validation against live ZB galaxy:
- $MESReceiver: 25 → 0 (correct: no ScriptExtensions on this template)
- $TestMachine: 1 entry — UpdateTestChangingInt with body
"Me.TestChangingInt = System.Random().Next(1,1000);" + all 7 fields
+ trigger metadata.
- $DelmiaReceiver: 2 entries — ProcessRecipe + Reset, both with bodies
("Me.RecipeDownloadFlag = false;") + trigger metadata.
61/61 existing tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ac3368993e |
mxaccesscli: capture user-attribution investigation as a pick-up note
Adds a dated incident-style file at the tool root recording the full arc of the User_Name attribution work over the last two days: - Six runs tabulated (galaxy mode x advise variant x write variant x resulting User_Name) so the next agent can see what's already been ruled out. - Current state of the CLI (auth, advise routing, WriteSecured) and the galaxy (eOSUserBased, ArchestraUsers role, engines deployed before the security change). - Leading hypothesis: running aaEngine processes still operate under the original eNone security context because galaxy security is compiled at deploy time. Until the platform/engines are redeployed, auth_user_id stays at 1 and User_Name stays NULL. - Concrete pick-up commands: undeploy/redeploy DevPlatform -> DevAppEngine -> TestArea -> TestMachine_001 via graccesscli, then re-run the trigger / ack-as-dohertj2 / clear sequence and query Events. - Fallbacks if a clean redeploy doesn't change the answer (aaBootstrap restart, two-person Verified Write, attribute security classification check, cross-tool comparison vs Object Viewer / InTouch). Linked from README.md's resource index so an agent landing on the tool finds the open thread without spelunking commit history. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c8f31bd653 |
mxaccesscli: add --secured / --verifier-* options for WriteSecured
WriteCommand grows three new options:
--secured Route the write through
LMXProxyServer.WriteSecured(currentUserId,
verifierUserId, value) instead of plain
Write(value, userId). Required for attributes
classified as Secured Write or Verified Write,
and useful for testing whether the audit
subsystem propagates user identity when
explicitly told the write is "secured".
--verifier-username Galaxy / OS username of the verifier for a
two-person Verified Write. Implies --secured.
--verifier-domain Domain composed with --verifier-username as
'<domain>\<username>'.
--verifier-password Verifier password. Redacted in the JSON
query echo.
When --secured is on without a verifier, the same auth_user_id is
used for both currentUserId and verifierUserId (single-user Secured
Write semantics). When a verifier is provided, the CLI authenticates
both users and bails cleanly with "verifier-authentication-failed"
on a verifier credential mismatch.
The JSON envelope's results[] gains `secured` and `verifier_user_id`
fields so an agent can confirm which path ran.
MxItem grows WriteSecured(value, currentUserId, verifierUserId).
Verified live against TestMachine_001.TestAlarm002.AckMsg under
eOSUserBased + ArchestraUsers role: --secured succeeds with
auth_user_id=1, verifier_user_id=1, MxCategoryOk. User_Name in the
Historian Events row remains NULL — same as plain Write. The
audit-attribution gate is not Write vs WriteSecured; running engines
likely still need a redeploy to pick up the new security mode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
68eb9adae7 |
mxaccesscli: catch ArgumentException from AuthenticateUser as auth-failed
Under eOSUserBased galaxy security mode the LMXProxyServer raises
ArgumentException("Value does not fall within the expected range")
for bad credentials instead of silently returning 0 like permissive
(eNone) galaxies do. Both shapes mean "auth failed"; MxSession.Authenticate
now normalizes them into a 0 return so WriteCommand reports a clean
"authentication-failed" envelope and exits 1 — instead of crashing
with a stack trace.
Verified live against the ZB galaxy in eOSUserBased mode:
bad password -> ok=false, error="authentication-failed", exit 1
good password -> ok=true, auth_user_id=1, exit 0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a842f67b54 |
mxaccesscli: document session-scoped userId semantics
Replaces the "Reusing a userId" subsection with a clearer "userId is session-scoped" note that captures the actual GRAccess/MxAccess contract: - AuthenticateUser returns an int that is bound to the LMX session hServer it was issued against, exactly like tag handles from AddItem. When the session unregisters (i.e. the MxSession is disposed, which happens when the process exits) the userId is no longer valid. - Each mxa write invocation creates a fresh MxSession, so an authenticated write must include --username/--password (or a userId resolved within the current process). - --user-id <N> is a hand-off for in-process callers, not a persistable credential. This matches the user's observation that userIds behave like tag/item handles in scope, and explains why batch flows benefit from a future session-mode mxa daemon (modeled on graccesscli's session) that holds one MxSession across many commands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9de8660688 |
mxaccesscli: route write through Advise vs AdviseSupervisory by user
WriteCommand now picks the LMXProxyServer advise variant based on
whether credentials were supplied:
--username given -> Advise (operator action; the write
is attributed to the
authenticated Galaxy user
in the alarm/event audit
trail)
no --username -> AdviseSupervisory (supervisory action; the
write is attributed to the
hosting client itself, no
Galaxy user claimed)
MxItem grows AdviseSupervisory() alongside Advise() and shares the
same UnAdvise / RemoveItem teardown.
Verified live with the trigger / ack-as-dohertj2 / clear sequence on
TestMachine_001.TestAlarm002. The Set (anonymous, supervisory) and
Clear (anonymous, supervisory) rows pair with the Acknowledged row
(authenticated, Advise) under one Alarm_ID. On this development
galaxy every action still maps to User_Name=DefaultUser regardless
of advise variant — that's a galaxy-security configuration trait,
not a CLI bug. The routing is in place and will differentiate
correctly on a strict galaxy with real user records.
docs/usage.md gains an "Advise variant" section explaining the rule
and the expected User_Name population on strict vs permissive
galaxies.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0d25ec445f |
mxaccesscli: add --username / --domain / --password to write
Wraps LMXProxyServer.AuthenticateUser at the session level
(MxSession.Authenticate) and surfaces three new options on the
write command:
-u, --username <name> galaxy / OS user
--domain <name> composed as <domain>\<username>; omit for
galaxy-authenticated logins or UPN forms
-p, --password <pwd> redacted to "***" in the JSON query echo
The CLI calls AuthenticateUser before AddItem so an auth failure
(userId == 0) bails out cleanly without leaving a half-set-up item.
On success the resolved userId flows into Write(hServer, hItem,
value, userId) and is reflected in:
- human output: "[OK ] write <tag> = <val> (as <verify-user>, userId=N)"
- LLM-JSON results[]: { "authenticated": true, "auth_user_id": N }
Verified live against TestChildObject.TestInt with credentials
DESKTOP-6JL3KKO\dohertj / Sonamu89:
read -> 99
write 7 with --username/--domain/--password -> ok, auth_user_id=1
read -> 7
write 99 with same auth -> ok
read -> 99 (restored)
Important behavior surfaced and documented in docs/usage.md
"Authentication" section: on a galaxy configured in permissive
(Free Access) mode, AuthenticateUser returns a non-zero userId
regardless of credentials — verified by intentionally passing a
wrong password and an unknown user, both of which still resolved
to userId=1 and the write went through. The CLI's auth path is
wired correctly; the galaxy just isn't strict. To exercise real
authentication, target a galaxy with galaxyAuthenticationMode and
attribute-level security above Free Access.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
7da30cf515 |
mxaccesscli: support bulk array writes via <arrayAttr>[]
WriteCommand now accepts multiple positional values when the tag reference ends with '[]', bundling them into a strongly-typed array (string[], int[], bool[], etc.) before passing to MxAccess.Write. The CLR marshals the array to a COM SAFEARRAY of the matching VARTYPE, which is the shape MxAccess expects for an array attribute. Verified live on a 50-slot String[] (MESReceiver_001.MoveInPartNumbers): write 50 distinct strings A1..A50 -> ok, MxCategoryOk read [] -> ['A1','A2', ..., 'A50'] Plus a guardrail: passing multiple values without the '[]' suffix exits 2 with a clear error so a typo can't accidentally write only the first element of an indexed reference. Critical finding documented in docs/usage.md: **a bulk write resizes the array to the count provided.** Writing 25 values into a 50-slot array leaves the array at 25 elements; the trailing 25 are deallocated, not zero-filled. Verified by 50 -> 25 -> 50 round-trip on the same attribute. Discover the runtime length via 'mxa read <attr>[]' or the configured length via grdb's attributes.sql array_dimension column. Type matrix in docs/usage.md updated: - Bulk array via '[]' - read ✅ + write ✅ - Bare reference (no brackets) - read ❌ + write ❌ - Element via '[N]' - unchanged ValueCoercion.cs: adds CoerceArray(IReadOnlyList<string>, typeHint) that produces strongly-typed arrays. Default element type is inferred from the first value when --type is unspecified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e9e37385aa |
mxaccesscli: document <arrayAttr>[] as the bulk-array read syntax
Empty square brackets — `<obj>.<arrayAttr>[]` — are the supported
MxAccess reference form for fetching a whole array as a single value.
Verified live:
mxa read 'MESReceiver_001.MoveInPartNumbers[]' --llm-json
-> ok=true, value=["", "11111", "", ..., "15", ...] (50 elements,
quality=192, MxCategoryOk)
The bare reference without brackets continues to return
MxCategoryCommunicationError, Detail=1003 — that's a real proxy-side
rejection, not a CLI bug. Documented separately so the matrix shows
both the working syntax and the failing one.
docs/usage.md:
- Type matrix gains a "bulk array read via []" row (✅) and a
"bare reference" row (❌, kept for reference).
- "Reading arrays" section rewritten around the two reference
shapes — `[]` for whole arrays, `[N]` for elements (1-based,
N <= NumElements).
- Adds a "Discovering array length" note pointing at grdb's
attributes.sql array_dimension column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
cc1ce05f96 |
mxaccesscli: document type/array support matrix from live verification
Adds a 'Type support matrix' table to docs/usage.md capturing what was
verified against the live ZB galaxy on System Platform 2017 Express
with MxAccess 3.2.0.0:
Verified read + write (round-tripped):
- MxBoolean — DelmiaReceiver_001.TestAttribute (false→true→false)
- MxInteger — MESReceiver_001.MoveInBatchID (999→12345→0→999)
- MxString — MESReceiver_001.MoveOutErrorText (round-trip + restore)
- String array element via 1-based [N] indexing
— MESReceiver_001.MoveInPartNumbers[2] (11111→ARR-WRITE-OK→11111)
Verified read only (no writeable instance in this galaxy):
- MxFloat — DevPlatform.CPULoad{,Avg,Max,Min}, PageFaultsAvg
- MxTime — DevPlatform.SystemStartupTime → ISO-8601 string
- MxReferenceType — Container/Host/Area → target object's Tagname
Wired but no live instance found (matrix marked '❓'):
- MxDouble, MxElapsedTime, MxQualifiedEnum, MxInternationalizedString,
MxBigString. Adding a writeable UDA of any of these types should
exercise the existing code path without further changes.
Hard limitation surfaced and documented:
- MxAccess.AddItem('<obj>.<arrayAttr>') for bulk array read returns
MxCategoryCommunicationError, Detail=1003. Read array elements
individually via the 1-based '[N]' suffix; '[0]' is invalid.
Adds a 'Reading arrays' section pointing at grdb's attributes.sql
('array_dimension' column) for discovering element count.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
7cbe2756d0 |
mxaccesscli: clean up diag output
Rewrites DiagCommand to match the structure of read/write/subscribe:
- Output goes through CliFx's IConsole instead of System.Console
(kills the 6 CliFx_SystemConsoleShouldBeAvoided build warnings).
- Single concise summary line at the top: `diag <tag> (<sec>s, <status>)`.
- Status now correctly distinguishes ok / error / no-events. A bogus
reference produces a DataChange event with MxCategoryConfigurationError;
that reports as "error", not the previous (incorrect) "ok".
- Adds --llm-json mode mirroring read/write: { query, ok, results: [...] }
with register/add_item/events/totals fields.
- Captures both DataChange and OperationComplete events with status
detail rendered consistently.
- Drops the duplicate "Done. Total events: N" / "diag result: events=N"
trailing lines.
Also fixes exit-code propagation from soft-failure commands. Environment
.ExitCode set inside ExecuteAsync was being overwritten by CliFx's own
return from RunAsync. Program.Main now folds the two:
return cliFxExit != 0 ? cliFxExit : Environment.ExitCode
This restores correct exit-on-failure behavior for both `diag` (error
status) and `write` (timeout / non-Ok ack) without resorting to
CommandException, which would print "ERROR\n<stack trace>" even for an
empty message.
Verified:
diag TestChildObject.TestInt -> EXIT 0, "ok"
diag NoSuchObject.NoSuchAttr -> EXIT 1, "error"
diag --llm-json -> structured envelope, ok flag honors statuses
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ab202a1fa1 |
mxaccesscli: read/write/subscribe System Platform tags via MxAccess
New tool wrapping ArchestrA.MxAccess.LMXProxyServerClass (the same COM
proxy aaObjectViewer / WindowViewer use) as a CliFx CLI for LLM-driven
debugging.
Commands:
- mxa info — loaded MxAccess assembly identity, supported value
types, MxStatusCategory enum.
- mxa read — fetch one or more tag values; subscribes briefly,
captures first OnDataChange per tag, tears down.
- mxa write — write a value with optional --type coercion; advises
first to resolve the attribute type, then waits for
OnWriteComplete with a per-call timeout.
- mxa subscribe — stream OnDataChange events for --seconds; JSON Lines
under --llm-json for piped agent consumption.
- mxa diag — minimal smoke test on a private STA thread; bypasses
the CliFx pipeline for diagnosing apartment / pump
issues.
Implementation notes documented in docs/api-notes.md (reverse-engineered
because AVEVA does not publish a single canonical MxAccess reference):
- Net48 / x86 / [STAThread] are non-negotiable. The CLI runs the entire
CliFx pipeline on a dedicated STA thread.
- COM events are dispatched as Win32 messages; AutoResetEvent.WaitOne
alone does not pump them on this configuration. MxSession.WaitForUpdate
loops Application.DoEvents() + drain + Sleep(20ms) instead.
- Write requires the target attribute's type to be resolved first.
WriteCommand advises and waits for the initial OnDataChange before
calling LMXProxyServerClass.Write to avoid ArgumentException
"Value does not fall within the expected range".
- Errors carry the full MXSTATUS_PROXY[] from MxAccess (Success,
Category, DetectedBy, Detail) so an agent can tell exactly which
layer rejected a request.
Verified against the live ZB galaxy with a writeable tag identified
via grdb (TestChildObject.TestInt, mx_attribute_category=10):
read: 99 (q=192, MxCategoryOk)
write 7: round-tripped — read returned 7 — written back to 99
write str: TestChildObject.TestString round-tripped a timestamp
subscribe: captured initial value plus subsequent change from a
separate process
The vendored ArchestrA.MxAccess.dll is gitignored — it is copied from
C:\Program Files (x86)\ArchestrA\Framework\Bin\ on any System Platform
install per the README.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
5a78ec5a76 |
histdb: add connectioninfo.md with verified Runtime DB access
Captures the working SQL Server connection state on this machine: server (localhost / MSSQLSERVER), database (Runtime), Windows-auth account (DESKTOP-6JL3KKO\dohertj2, sysadmin), the sqlcmd recipe, four paste-and-run sanity probes including an end-to-end INSQL retrieval, linked-server roles (INSQL, INSQLD), and alternate client paths (SSMS / PowerShell / ODBC / pyodbc). Linked from histdb/README.md Layout and Resource index per DOCS-GUIDE.md. Mirrors the pattern grdb/connectioninfo.md uses for its own DB. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
32f26272ae |
Initial commit: Wonderware / System Platform tools and reference
Five tools under one repo, all docs organized per DOCS-GUIDE.md: - aalogcli: .NET 4.8 / x86 CliFx CLI for reading System Platform binary logs (*.aaLGX) for LLM debugging, built on aaOpenSource/aaLog. Commands: last, tail, range, unread, fields. Stable JSON envelope under --llm-json. Build template under lib/build/ for rebuilding aaLogReader.dll. - aot: ArchestrA Object Toolkit 2014 v4.0 reference material. Dev guide (Markdown converted from CHM), API reference for the ArchestrA.Toolkit namespace, and the Monitor / Watchdog VS sample solutions. - graccesscli: .NET 4.8 / x86 CliFx CLI that automates Galaxy configuration via the ArchestrA GRAccess COM interop. Includes session daemon, IPC protocol, and llm-json envelope contract. - grdb: SQL/DDL exploration of the Galaxy Repository database. DDL captures, reusable queries, hierarchy / contained-name <-> tag-name translation notes. - histdb: LLM-oriented reference for AVEVA Historian retrieval. INSQL linked-server, extension tables, every wwXxx time-domain extension, every retrieval mode, alarm/event SQL recipes, REST API. Distilled from the 243-page Historian Retrieval Guide. Root contains: - CLAUDE.md: thin index pointing into each tool's README. - DOCS-GUIDE.md: doctrine for organizing docs for LLM consumption. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |