Files
wwtools/mxaccesscli/AGENTS.md
T
Joseph Doherty 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>
2026-05-03 20:02:51 -04:00

83 lines
5.6 KiB
Markdown

# AGENTS.md
Guidance for coding agents working in the `mxaccesscli` folder.
## Project Snapshot
`mxaccesscli` (assembly name `mxa`) is a `.NET Framework 4.8 / x86` CliFx CLI that wraps `ArchestrA.MxAccess.LMXProxyServerClass` to read, write, and subscribe to AVEVA System Platform tags. Output as human-readable lines or as a stable `{ query, ok, results }` JSON envelope under `--llm-json`.
For end-to-end usage and examples, read [`docs/usage.md`](docs/usage.md). For the underlying MxAccess COM API surface (assembly is shipped without public documentation), read [`docs/api-notes.md`](docs/api-notes.md).
## Key Documentation
All paths relative to this `mxaccesscli/` folder.
- [`README.md`](README.md) — tool entry point, hard constraints, build instructions.
- [`docs/usage.md`](docs/usage.md) — every command, every option, examples, JSON envelope shape.
- [`docs/api-notes.md`](docs/api-notes.md) — reverse-engineered MxAccess API reference (types, events, threading model, status semantics).
## Repository Layout
```text
mxaccesscli/
MxAccess.Cli.slnx
lib/ArchestrA.MxAccess.dll (copied from System Platform's Framework\Bin)
src/MxAccess.Cli/
MxAccess.Cli.csproj
Program.cs [STAThread] entry; wires CliFx commands
IsExternalInit.cs net48 polyfill for C# 9 init accessors
Commands/
ReadCommand.cs `read` — first OnDataChange per tag, timed
WriteCommand.cs `write` — write + OnWriteComplete, timed
SubscribeCommand.cs `subscribe` — stream OnDataChange for N seconds
InfoCommand.cs `info` — assembly identity + value-type catalog
Mx/
MxSession.cs Register/Unregister + event pump + queue
MxItem.cs AddItem / Advise / UnAdvise / RemoveItem pairing
MxUpdate.cs typed wrapper over OnDataChange / OnWriteComplete
ValueCoercion.cs --type bool|int|double|... → boxed object for Write
Output/
Envelope.cs { query, ok, results } JSON shape
```
## Build And Test
Run from this `mxaccesscli` folder.
```powershell
dotnet build src/MxAccess.Cli/MxAccess.Cli.csproj -p:Platform=x86 -c Release
dotnet run --project src/MxAccess.Cli/MxAccess.Cli.csproj -- <args>
```
There is no test project. If you add one, mirror `graccesscli`'s pattern: `tests/MxAccess.Cli.Tests/`, `net48`, `x86`, xunit + Shouldly. Pure unit-testing of `MxSession` is hard because `LMXProxyServerClass` is sealed and COM-bound; use a thin `IMxSession` interface if you need test seams.
## Implementation Rules
- **Stay on `net48` / `x86` / `[STAThread]`.** MxAccess is a 32-bit COM proxy and events fire on the apartment that called `Register()`. Crossing apartments will silently lose callbacks.
- **Always pair Register / Unregister, AddItem / RemoveItem, Advise / UnAdvise.** `MxSession.Dispose` and `MxItem.Dispose` already do this — leverage `using` blocks.
- **Reads are not synchronous in MxAccess.** Implement "read" as a brief Advise → first OnDataChange → tear down. Document the timeout semantic in user-facing surfaces.
- **Surface `MxStatusCategory` on every error.** A non-Ok / non-Pending category in any element of `MXSTATUS_PROXY[]` means failure. Don't collapse the array — emit it whole in the JSON envelope so an agent can tell *why* something failed.
- **`Write` uses an `int userId`.** Use 0 for unauthenticated operations; for secured attributes call `AuthenticateUser()` first (the CLI does not yet expose this — add it through `WriteCommand` if needed and document it).
- **Don't reuse `LMXProxyServerClass` instances across runs.** Each `MxSession` constructs and disposes one. The proxy is cheap enough that one-shot CLI invocations are fine; for many writes back-to-back, build a session-mode subcommand modeled on `graccesscli`.
- **CliFx command pattern.** Commands implement `ICommand`, decorate with `[Command]`, parameters with `[CommandParameter]`, options with `[CommandOption]`. C# 9 `init` accessors via `IsExternalInit.cs` polyfill; no `required`.
- **JSON envelope contract.** Every `--llm-json` command emits `{ query: {...}, ok: bool, results: [...] }`. `query` echoes the resolved invocation (including coerced value types). `results` is always an array, even for single-tag commands. Adding fields to a `result` is non-breaking; renaming/removing is.
## Output Contracts
| Mode | Trigger | Shape |
| --- | --- | --- |
| Human (read/write) | default | One line per tag: `[OK ] <tag> = <value> (q=<quality>, t=<ts>)` or `[ERR] <tag>: <reason>` |
| Human (subscribe) | default | One line per event: `[<HH:mm:ss.fff>] [OK ] <tag> = <value> (q=<quality>)` |
| LLM-JSON (read/write) | `--llm-json` | `{ query, ok, results: [ { tag, ok, value?, quality?, timestamp?, statuses, error? } ] }` |
| LLM-JSON (subscribe) | `--llm-json` | **JSON Lines** — one JSON object per OnDataChange event, no envelope wrapper. Allows piped consumption. |
The subscribe-stream JSON Lines format is intentionally different from the read/write envelope — streams need to be consumable line-by-line.
## Adding A New Command
1. Add `Commands/<Name>Command.cs` implementing `ICommand`.
2. Construct an `MxSession` in a `using` block; build `MxItem` instances; dispose them before `MxSession`.
3. Use `MxSession.WaitForUpdate(predicate, timeout, out update)` for one-shot waits, or `DrainUpdates()` + `WaitOnce()` in a loop for streams.
4. Add a row in [`README.md`](README.md) and a section in [`docs/usage.md`](docs/usage.md). Do not modify `../CLAUDE.md` — the root index points at the README and that is sufficient.
5. Keep envelope shape `{ query, ok, results }` consistent across read/write commands.