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>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
# MxAccess API notes
|
||||
|
||||
Reverse-engineered from the shipped `ArchestrA.MxAccess.dll` (assembly version `3.2.0.0`, file at `C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`) plus its sibling type libraries `MXAccess.tlb` / `MXAccess20.tlb` / `MXAccess32.tlb`. AVEVA does not publish a single canonical online reference for this assembly, which is why this file exists.
|
||||
|
||||
The assembly is registered in the GAC at `C:\Windows\assembly\GAC_MSIL\ArchestrA.MxAccess\3.2.0.0__23106a86e706d0ae\`. Strong name: `PublicKeyToken=23106a86e706d0ae`.
|
||||
|
||||
## Threading model
|
||||
|
||||
`LMXProxyServerClass` is a COM proxy. **All calls — and the events the proxy fires — run on the apartment that called `Register`.** The CLI's `Program.Main` is `[STAThread]` for this reason; if you call into MxAccess from a non-STA thread you'll observe:
|
||||
|
||||
- `Register()` returns a handle but events never fire.
|
||||
- `Write()` queues but `OnWriteComplete` never reaches your handler.
|
||||
|
||||
A deeper STA pump is unnecessary for one-shot CLI invocations because CliFx's `ICommand.ExecuteAsync` runs on the entry thread synchronously, and the COM marshaller pumps the calling apartment while waiting on `WaitOne`.
|
||||
|
||||
## Type catalog
|
||||
|
||||
The full public surface from reflection:
|
||||
|
||||
```text
|
||||
class LMXProxyServerClass -- the entry point (newable)
|
||||
class LMXProxyServer -- alias / synonym
|
||||
iface ILMXProxyServer -- v1 method set (Register, AddItem, Advise, Write, WriteSecured, AuthenticateUser)
|
||||
iface ILMXProxyServer2 -- adds ArchestrAUserToId
|
||||
iface ILMXProxyServer3 -- adds AddItem2
|
||||
iface ILMXProxyServer4 -- adds Write2, WriteSecured2, Suspend, Activate, AdviseSupervisory
|
||||
iface ILMXProxyServer5 -- adds AddBufferedItem, SetBufferedUpdateInterval
|
||||
|
||||
iface _ILMXProxyServerEvents -- OnDataChange, OnWriteComplete, OperationComplete
|
||||
iface _ILMXProxyServerEvents2 -- OnBufferedDataChange
|
||||
|
||||
struct MxStatus -- success:int16, category:MxStatusCategory, detectedBy:MxStatusSource, detail:int16
|
||||
struct MXSTATUS_PROXY -- same fields; passed to event handlers as an array
|
||||
enum MxStatusCategory -- 10 values; see `mxa info`
|
||||
enum MxStatusSource -- 7 values; "who detected this status"
|
||||
enum MxDataType -- MxNoData / MxBoolean / MxInteger / MxFloat / MxDouble / MxString / MxTime / MxElapsedTime / MxReferenceType / MxStatusType / MxDataTypeEnum / MxSecurityClassificationEnum / MxDataQualityType / MxQualifiedEnum / MxQualifiedStruct / MxInternationalizedString / MxBigString / MxDataTypeEND / MxDataTypeUnknown
|
||||
```
|
||||
|
||||
## Lifecycle methods
|
||||
|
||||
```csharp
|
||||
int Register(string clientName); // returns hServer
|
||||
void Unregister(int hServer);
|
||||
|
||||
int AddItem(int hServer, string itemRef); // returns hItem
|
||||
int AddItem2(int hServer, string itemRef, string itemContext);
|
||||
int AddBufferedItem(int hServer, string itemRef, string itemContext);
|
||||
void RemoveItem(int hServer, int hItem);
|
||||
|
||||
void Advise(int hServer, int hItem);
|
||||
void UnAdvise(int hServer, int hItem);
|
||||
void AdviseSupervisory(int hServer, int hItem); // higher-rate / mxsupv updates
|
||||
void SetBufferedUpdateInterval(int hServer, int intervalMs);
|
||||
```
|
||||
|
||||
`Suspend` / `Activate` toggle update delivery on a per-item basis without losing the AddItem handle.
|
||||
|
||||
## Write methods
|
||||
|
||||
```csharp
|
||||
void Write (int hServer, int hItem, object value, int userId);
|
||||
void Write2 (int hServer, int hItem, object value, object timestamp, int userId);
|
||||
void WriteSecured (int hServer, int hItem, int currentUserId, int verifierUserId, object value);
|
||||
void WriteSecured2 (int hServer, int hItem, int currentUserId, int verifierUserId, object value, object timestamp);
|
||||
```
|
||||
|
||||
`userId = 0` is unauthenticated. For attributes with **Operate**, **Tune**, **Configure**, **View Only**, **Free Access**, **Secured Write**, or **Verified Write** classifications (see [`../../aot/dev-guide/appendix-e-security-classifications.md`](../../aot/dev-guide/appendix-e-security-classifications.md)):
|
||||
|
||||
- **Free Access** — `Write` succeeds with `userId = 0`.
|
||||
- **Operate / Tune / Configure** — `Write` requires a real `userId` from `AuthenticateUser`.
|
||||
- **Secured Write** — must call `WriteSecured` with the same user as both `currentUserId` and `verifierUserId` (or use `Write` after `AuthenticateUser` if Galaxy security allows).
|
||||
- **Verified Write** — must call `WriteSecured` with two distinct authenticated user ids (operator + verifier).
|
||||
|
||||
## Authentication helpers
|
||||
|
||||
```csharp
|
||||
int AuthenticateUser(int hServer, string verifyUser, string verifyUserPwd);
|
||||
int ArchestrAUserToId(int hServer, string userIdGuid);
|
||||
```
|
||||
|
||||
`AuthenticateUser` returns a non-zero user id when credentials match the Galaxy security configuration, or 0 on failure. `ArchestrAUserToId` resolves a Galaxy user GUID to its numeric id.
|
||||
|
||||
## Events
|
||||
|
||||
All events fire on the registering apartment. The CLI bridges them to a `ConcurrentQueue<MxUpdate>` plus an `AutoResetEvent` so command code uses `WaitForUpdate(predicate, timeout, out update)` rather than dealing with the COM event signature directly.
|
||||
|
||||
```csharp
|
||||
event OnDataChange(int hServer, int hItem,
|
||||
object value, int quality, object timestamp,
|
||||
ref MXSTATUS_PROXY[] statuses);
|
||||
|
||||
event OnWriteComplete(int hServer, int hItem, ref MXSTATUS_PROXY[] statuses);
|
||||
|
||||
event OperationComplete(int hServer, int hItem, ref MXSTATUS_PROXY[] statuses);
|
||||
// fires on AddItem completion etc.
|
||||
|
||||
event OnBufferedDataChange(int hServer, int hItem, MxDataType dataType,
|
||||
object value, object quality, object timestamp,
|
||||
ref MXSTATUS_PROXY[] statuses);
|
||||
// only when AddBufferedItem was used
|
||||
```
|
||||
|
||||
`timestamp` arrives as a Win32 `FILETIME` boxed inside an `object` (decimal/Int64-shaped). The CLI converts via `DateTime.FromFileTimeUtc(Convert.ToInt64(...))` and exposes it as a local-time `DateTime?`; if conversion fails the field is `null`.
|
||||
|
||||
`quality` on `OnDataChange` is the legacy 16-bit OPC quality value (e.g. `192 = 0xC0` for "Good"). The richer state lives in the `statuses[]` array, especially `Category` and `DetectedBy`.
|
||||
|
||||
## Status semantics
|
||||
|
||||
`MxStatusCategory` values, in roughly ascending severity:
|
||||
|
||||
| Category | Meaning |
|
||||
| --- | --- |
|
||||
| `MxCategoryOk` | Operation succeeded. |
|
||||
| `MxCategoryPending` | Operation accepted, working on it; not an error. |
|
||||
| `MxCategoryWarning` | Soft issue (e.g. value clamped); operation proceeded. |
|
||||
| `MxCategoryCommunicationError` | LMX/NMX / engine reach failure. |
|
||||
| `MxCategoryConfigurationError` | Reference unresolved, attribute missing, type mismatch. |
|
||||
| `MxCategoryOperationalError` | Object not OnScan, attribute read-only, etc. |
|
||||
| `MxCategorySecurityError` | Secured / Verified attribute and credentials insufficient. |
|
||||
| `MxCategorySoftwareError` | Bug in the LMX runtime — usually rare. |
|
||||
| `MxCategoryOtherError` | Catch-all. |
|
||||
| `MxStatusCategoryUnknown` | Couldn't classify. |
|
||||
|
||||
`MxStatusSource` tells you *who* set the category — useful when the same code (e.g. `MxCategoryConfigurationError`) can come from the requesting LMX (your client side) versus the responding automation object (the server side):
|
||||
|
||||
```
|
||||
MxSourceRequestingLmx - this CLI's local LMX
|
||||
MxSourceRespondingLmx - the LMX hosting the target
|
||||
MxSourceRequestingNmx - cross-node messaging on this side
|
||||
MxSourceRespondingNmx - cross-node messaging on the target side
|
||||
MxSourceRequestingAutomationObject
|
||||
MxSourceRespondingAutomationObject
|
||||
MxSourceUnknown
|
||||
```
|
||||
|
||||
A `statuses` array often has multiple entries — one per layer that touched the request. The CLI emits all of them so an agent can pinpoint which layer rejected the call.
|
||||
|
||||
## Buffered items
|
||||
|
||||
`AddBufferedItem` returns updates batched at `SetBufferedUpdateInterval` (milliseconds) intervals via `OnBufferedDataChange`. Useful for high-rate tags where you want time-bucketed snapshots instead of every tick. The CLI does not expose this surface yet — adding a `subscribe --buffered <ms>` option is straightforward.
|
||||
|
||||
## Type libraries
|
||||
|
||||
| File | Use |
|
||||
| --- | --- |
|
||||
| `MXAccess.tlb` | v1 — minimal interface, legacy clients. |
|
||||
| `MXAccess20.tlb` | adds events. |
|
||||
| `MXAccess32.tlb` | full v3.x surface — what `ArchestrA.MxAccess.dll 3.2.0.0` projects. |
|
||||
|
||||
If you need to consume MxAccess from a different language (C++, Python via comtypes, etc.), import `MXAccess32.tlb` rather than the older versions — they expose subsets.
|
||||
|
||||
## What the CLI does not (yet) cover
|
||||
|
||||
- `Suspend` / `Activate` — easy to add as a sub-command.
|
||||
- `AddItem2` with explicit context — the CLI passes a single tag reference; if your environment needs context (cross-galaxy, role-based) wrap it.
|
||||
- `WriteSecured` / `WriteSecured2` — adding a `--verifier-id <int>` option to `WriteCommand` is the natural extension.
|
||||
- `AdviseSupervisory` and `AddBufferedItem` — opt-in performance modes.
|
||||
@@ -0,0 +1,142 @@
|
||||
# mxa — usage
|
||||
|
||||
Read, write, and subscribe to AVEVA System Platform tags via MxAccess. The CLI runs in-process: each invocation registers an `LMXProxyServer`, executes, and unregisters cleanly. Errors carry the underlying `MxStatusCategory` so an agent can decide whether the failure is transient (Pending), configurational, or operational.
|
||||
|
||||
## Common notes
|
||||
|
||||
- **Tag references** are full attribute paths: `<ObjectName>.<AttributeName>` (e.g. `TestMachine_001.Speed`). For `Galaxy:` references, follow the convention used in InTouch / Object Viewer.
|
||||
- **`--client <name>`** sets the client name passed to MxAccess `Register()`. Defaults to `mxa`. Most install logs key on this string.
|
||||
- **Timeouts are per-call.** They control how long the CLI waits for a `OnDataChange` (read) or `OnWriteComplete` (write). The default is 5 seconds.
|
||||
- **First-event latency.** LMX has to resolve the reference and bind to the hosting engine on each fresh client connection. Empirically the first `OnDataChange` arrives **3-8 seconds** after `Advise()`. Set timeouts and `subscribe --seconds` accordingly: a 3-second `read` may legitimately time out on first contact, then succeed on the next try because LMX has cached the binding.
|
||||
- **Subsequent events are fast.** Once a tag is bound, value-change updates propagate within ~100 ms.
|
||||
- **Exit codes:** `0` on success, `1` if any operation timed out or returned a non-Ok / non-Pending `MxStatusCategory`, `2` on argument-validation errors.
|
||||
|
||||
## `mxa info`
|
||||
|
||||
Print the loaded `ArchestrA.MxAccess` assembly identity, supported `--type` values, and the full `MxStatusCategory` enum. No tag access.
|
||||
|
||||
```powershell
|
||||
mxa info
|
||||
```
|
||||
|
||||
## `mxa read <tag> [<tag>...]`
|
||||
|
||||
Reads one or more tags by briefly subscribing and capturing the first `OnDataChange` per tag.
|
||||
|
||||
| Option | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `-t`, `--timeout <seconds>` | `5` | Per-tag timeout. Tags that don't deliver a `DataChange` within the window are reported with `error: timeout`. |
|
||||
| `--client <name>` | `mxa` | Passed to `Register()`. |
|
||||
| `--llm-json` | off | Emit the JSON envelope. |
|
||||
|
||||
Examples:
|
||||
|
||||
```powershell
|
||||
mxa read TestMachine_001.Speed
|
||||
mxa read TestMachine_001.Speed Reactor1.Level -t 3
|
||||
mxa read TestMachine_001.Speed Reactor1.Level --llm-json
|
||||
```
|
||||
|
||||
LLM-JSON envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"query": { "command": "read", "tags": ["TestMachine_001.Speed"], "timeout_s": 5.0, "client": "mxa" },
|
||||
"ok": true,
|
||||
"results": [
|
||||
{
|
||||
"tag": "TestMachine_001.Speed",
|
||||
"ok": true,
|
||||
"value": 1234.5,
|
||||
"quality": 192,
|
||||
"timestamp": "2026-05-03T19:42:18.001",
|
||||
"statuses": [
|
||||
{ "Success": 0, "Category": "MxCategoryOk", "DetectedBy": "MxSourceRespondingAutomationObject", "Detail": 0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## `mxa write <tag> <value>`
|
||||
|
||||
Writes one value to one tag and waits for `OnWriteComplete`.
|
||||
|
||||
| Option | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `--type <kind>` | inferred | Force the .NET type used for the boxed value. One of `bool`, `byte`, `short`, `int`, `long`, `float`, `double`, `string`, `datetime`. |
|
||||
| `-t`, `--timeout <seconds>` | `5` | How long to wait for `OnWriteComplete`. |
|
||||
| `--user-id <int>` | `0` | Authenticated user id. `0` is unauthenticated; secured attributes will reject. |
|
||||
| `--client <name>` | `mxa` | Passed to `Register()`. |
|
||||
| `--llm-json` | off | Emit the JSON envelope. |
|
||||
|
||||
Type inference rules (when `--type` is not set): `true`/`false`/`yes`/`no`/`on`/`off`/`1`/`0` → bool; pure integer → `int` (then `long`); decimals → `double`; everything else → `string`.
|
||||
|
||||
Examples:
|
||||
|
||||
```powershell
|
||||
mxa write TestMachine_001.Setpoint 42.5 --type double
|
||||
mxa write TestMachine_001.RunFlag true
|
||||
mxa write TestMachine_001.Label "Hello world"
|
||||
mxa write Reactor1.Setpoint 100 --type int -t 10 --llm-json
|
||||
```
|
||||
|
||||
The same JSON envelope shape as `read`, with `results[0]` containing `{ tag, ok, error?, statuses }`. No `value`/`quality`/`timestamp` on the write result — consult a follow-up `mxa read` to confirm.
|
||||
|
||||
## `mxa subscribe <tag> [<tag>...]`
|
||||
|
||||
Streams `OnDataChange` events for a duration.
|
||||
|
||||
| Option | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `-s`, `--seconds <seconds>` | `10` | Wall-clock duration of the subscription. |
|
||||
| `--max <int>` | `1000` | Hard cap on emitted events. |
|
||||
| `--client <name>` | `mxa` | Passed to `Register()`. |
|
||||
| `--llm-json` | off | **JSON Lines** mode — one JSON object per line, no outer envelope. |
|
||||
|
||||
Human output:
|
||||
|
||||
```text
|
||||
[INFO] Subscribed to 1 tag(s). Streaming for 30.0s. Ctrl-C to stop early.
|
||||
[19:42:18.001] [OK ] TestMachine_001.Speed = 1234.5 (q=192)
|
||||
[19:42:19.002] [OK ] TestMachine_001.Speed = 1245.7 (q=192)
|
||||
...
|
||||
[INFO] 30 event(s) emitted; subscription closed.
|
||||
```
|
||||
|
||||
LLM-JSON output (one event per line, no surrounding `[ ... ]`):
|
||||
|
||||
```jsonl
|
||||
{"tag":"TestMachine_001.Speed","ok":true,"value":1234.5,"quality":192,"timestamp":"2026-05-03T19:42:18.001","statuses":[{...}]}
|
||||
{"tag":"TestMachine_001.Speed","ok":true,"value":1245.7,"quality":192,"timestamp":"2026-05-03T19:42:19.002","statuses":[{...}]}
|
||||
```
|
||||
|
||||
JSON Lines lets a downstream consumer parse events incrementally rather than buffering the whole stream — the right shape for indefinite or long-running subscriptions.
|
||||
|
||||
## Errors and statuses
|
||||
|
||||
Every `result` carries a `statuses` array — the elements of the COM `MXSTATUS_PROXY[]` MxAccess passes back. Field names match the C# struct exactly:
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `Success` | int16 | 0 = Ok, non-zero = error code |
|
||||
| `Category` | enum | `MxCategoryOk`, `MxCategoryPending`, `MxCategoryWarning`, `MxCategoryCommunicationError`, `MxCategoryConfigurationError`, `MxCategoryOperationalError`, `MxCategorySecurityError`, `MxCategorySoftwareError`, `MxCategoryOtherError`, `MxStatusCategoryUnknown` |
|
||||
| `DetectedBy` | enum | `MxSourceRequestingLmx`, `MxSourceRespondingLmx`, `MxSourceRequestingNmx`, `MxSourceRespondingNmx`, `MxSourceRequestingAutomationObject`, `MxSourceRespondingAutomationObject`, `MxSourceUnknown` |
|
||||
| `Detail` | int16 | Additional error-code detail |
|
||||
|
||||
A result is considered `ok` only if every `statuses` element has `Category in (MxCategoryOk, MxCategoryPending)`.
|
||||
|
||||
Common failure shapes:
|
||||
|
||||
- **`Category: MxCategoryConfigurationError`** — usually a typo'd reference or the attribute doesn't exist on the deployed instance. Sanity-check via `graccesscli object snapshot`.
|
||||
- **`Category: MxCategoryCommunicationError`** — engine isn't running, object is OffScan, or LMX can't reach the platform hosting the object.
|
||||
- **`Category: MxCategorySecurityError`** — secured attribute, `--user-id 0`. Use `WriteSecured` semantics (not yet exposed by this CLI) or target a `Writeable_USC_*` attribute.
|
||||
- **Timeout** — most likely the tag is genuinely silent (no value updates) or the reference is wrong. With `--llm-json` you'll see `"error": "timeout"` and an empty `statuses`.
|
||||
|
||||
## Picking a tag for a smoke test
|
||||
|
||||
If the live galaxy is not familiar:
|
||||
|
||||
1. Connect to the Galaxy Repository SQL — see [`../../grdb/connectioninfo.md`](../../grdb/connectioninfo.md).
|
||||
2. Find a deployed instance with a writeable UDA — [`../../grdb/queries/attributes.sql`](../../grdb/queries/attributes.sql) lists user-defined attributes with their data type. Filter on a `Writeable_*` security classification (see [`../../aot/dev-guide/appendix-e-security-classifications.md`](../../aot/dev-guide/appendix-e-security-classifications.md)).
|
||||
3. The reference for MxAccess is `<InstanceName>.<AttributeName>`.
|
||||
Reference in New Issue
Block a user