# 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: `.` (e.g. `TestMachine_001.Speed`). For `Galaxy:` references, follow the convention used in InTouch / Object Viewer. - **`--client `** 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 [...]` Reads one or more tags by briefly subscribing and capturing the first `OnDataChange` per tag. | Option | Default | Notes | | --- | --- | --- | | `-t`, `--timeout ` | `5` | Per-tag timeout. Tags that don't deliver a `DataChange` within the window are reported with `error: timeout`. | | `--client ` | `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 ` Writes one value to one tag and waits for `OnWriteComplete`. | Option | Default | Notes | | --- | --- | --- | | `--type ` | inferred | Force the .NET type used for the boxed value. One of `bool`, `byte`, `short`, `int`, `long`, `float`, `double`, `string`, `datetime`. | | `-t`, `--timeout ` | `5` | How long to wait for `OnWriteComplete`. | | `--user-id ` | `0` | Pre-resolved authenticated user id passed straight to `Write()`. Use only when you already have a userId. `0` = unauthenticated. | | `-u`, `--username ` | (none) | Galaxy / OS username. Combined with `--domain` (if set) into `\` and resolved to a userId via `AuthenticateUser` before `Write()`. See [Authentication](#authentication). | | `--domain ` | (none) | Domain or hostname for OS-authenticated galaxies. Combined with `--username` as `\`. Omit for galaxy-authenticated logins. | | `-p`, `--password ` | (none) | Password for `--username`. Redacted (`***`) in the LLM-JSON `query` echo. | | `--client ` | `mxa` | Passed to `Register()`. | | `--llm-json` | off | Emit the JSON envelope. Includes `authenticated` and `auth_user_id` fields when `--username` was supplied. | 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. ## Authentication Most galaxies require an authenticated `userId` to write attributes whose security classification is anything stricter than `Free Access` (i.e. `Operate`, `Tune`, `Configure`, `Secured Write`, `Verified Write` — see [`../../aot/dev-guide/appendix-e-security-classifications.md`](../../aot/dev-guide/appendix-e-security-classifications.md)). The `write` command resolves credentials to a `userId` by calling `LMXProxyServer.AuthenticateUser(verifyUser, password)`. The `verifyUser` string is composed from `--username` and `--domain`: | Galaxy mode | Pass | Composes to | | --- | --- | --- | | `osAuthenticationMode` (Windows / domain users) | `--username dohertj --domain DESKTOP-6JL3KKO` | `DESKTOP-6JL3KKO\dohertj` | | `galaxyAuthenticationMode` (galaxy-internal users) | `--username dohertj` (no `--domain`) | `dohertj` | | Mixed / AAD UPN | `--username dohertj@example.com` | `dohertj@example.com` | Example: ```powershell mxa write TestMachine_001.Setpoint 75.5 --type double ` --username dohertj --domain DESKTOP-6JL3KKO --password Sonamu89 ` --llm-json ``` A successful authentication populates two new fields in the JSON envelope's `results[]`: ```jsonc { "tag": "TestMachine_001.Setpoint", "ok": true, "authenticated": true, "auth_user_id": 17, // returned by AuthenticateUser; 0 means failure "statuses": [{"Category":"MxCategoryOk", ...}] } ``` The human output appends `(as , userId=N)` to the success line so the right credentials are visible in interactive use. ### Password handling `--password` is **redacted** to `***` in the LLM-JSON `query` echo and never logged in cleartext. It travels in-process from CliFx's argument parser straight into `AuthenticateUser` and is not persisted anywhere by the CLI. ### Failure modes | What you sent | What you get | | --- | --- | | Correct credentials, strict-mode galaxy | `authenticated=true`, `auth_user_id` > 0, write proceeds. | | Bad password, strict-mode galaxy | `auth_user_id == 0` → CLI exits 1 with `"error": "authentication-failed"`. No write attempted. | | Bad password, **permissive-mode galaxy** | The proxy returns a non-zero `auth_user_id` regardless. The CLI cannot tell this apart from a successful auth — it's the galaxy admin's responsibility to configure security strictly enough to reject. | | `--username` without `--password` | Sends an empty password. Some galaxies allow this; most don't. | > ⚠️ Verified behavior on the test galaxy used during development: `AuthenticateUser` returned `userId=1` for both the correct password and intentionally bad credentials (incl. an unknown username). This is consistent with a galaxy configured in `Free Access` mode where security checks are effectively disabled — the CLI's auth path is wired correctly, the galaxy just isn't strict. To exercise real authentication, target a galaxy with `galaxyAuthenticationMode` enabled and attribute-level security classifications above `Free Access`. ### Advise variant — operator vs supervisory `write` picks how it subscribes to the destination attribute (the briefly-active subscription used for type resolution before the Write call) based on whether you supplied credentials: | `--username` supplied? | Advise variant used | Audit-trail intent | | --- | --- | --- | | Yes | `LMXProxyServer.Advise` | Operator action — attribute the Write to the authenticated Galaxy user. | | No (anonymous) | `LMXProxyServer.AdviseSupervisory` | Supervisory action — attribute the Write to the hosting client (no Galaxy user claimed). | This affects how System Platform records the action in the alarm/event subsystem and the Historian's `Events` table. On a strict galaxy with `galaxyAuthenticationMode` and real user records: - Authenticated + `Advise` → `User_Name = `, `User_Account = \`. - Anonymous + `AdviseSupervisory` → `User_Name` typically NULL or the supervisory client identity. On a permissive galaxy (the development config used here), every action maps to `DefaultUser` regardless of advise variant — the mechanism is wired correctly but can't be differentiated until galaxy security is configured with real users. See [`Authentication`](#authentication) above. ### Reusing an already-resolved `userId` `AuthenticateUser` may be expensive (involves SQL Server lookup + Windows cred check). For batch scripts that issue many writes, call `AuthenticateUser` once via a manual call, capture the `userId`, then pass it directly via `--user-id ` to subsequent `write` invocations. This skips the per-call auth round-trip. ## `mxa subscribe [...]` Streams `OnDataChange` events for a duration. | Option | Default | Notes | | --- | --- | --- | | `-s`, `--seconds ` | `10` | Wall-clock duration of the subscription. | | `--max ` | `1000` | Hard cap on emitted events. | | `--client ` | `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. ## Type support matrix Verified end-to-end against the live `ZB` galaxy (System Platform 2017 Express, MxAccess `3.2.0.0`). Each row records what the wire shape looks like in the JSON envelope. | `MxDataType` | Read | Write | JSON shape | Notes | | --- | :---: | :---: | --- | --- | | `MxBoolean` | ✅ | ✅ | JSON `true` / `false` | `--type bool` accepts `true`/`false`/`1`/`0`/`yes`/`no`/`on`/`off`. | | `MxInteger` (Int32) | ✅ | ✅ | JSON number | `--type int`. Up-cast to `long` if it overflows `int.MaxValue`. | | `MxFloat` (single) | ✅ | ⚠️ | JSON number | Read verified on `DevPlatform.CPULoad` family. Write requires a writeable Float UDA — none in the test galaxy, but `--type float` is wired. | | `MxDouble` | ❓ | ❓ | JSON number | No accessible Double instance in the test galaxy. Wiring is identical to Float; expected to work. | | `MxString` | ✅ | ✅ | JSON string | Default inferred type for non-numeric values. | | `MxTime` (DateTime) | ✅ | ⚠️ | JSON string `"YYYY-MM-DDTHH:mm:ss"` | Read verified on `DevPlatform.SystemStartupTime`. Write via `--type datetime` accepts ISO-8601. | | `MxElapsedTime` | ❓ | ❓ | JSON number (seconds) | No accessible instance in the test galaxy. | | `MxReferenceType` | ✅ | – | JSON string (target object's `Tagname`) | E.g. `TestChildObject.Container` → `"DevTestObject"`. Writing references is not exposed by the CLI. | | `MxQualifiedEnum` (13) | ❓ | – | (likely string) | No accessible instance. | | `MxQualifiedStruct` (14) | – | – | – | Access via dotted member names: `..`. | | `MxInternationalizedString` (15) | ❓ | ❓ | (likely string) | No accessible instance. | | `MxBigString` (16) | ❓ | ❓ | JSON string | No accessible instance. | | **Array (any type), bulk read/write via `[]`** | ✅ | ✅ | JSON array of element type | Reference syntax `.[]` — **empty square brackets**. Read returns the entire array as a single value. Write takes one positional value per element (`mxa write '.[]' v1 v2 v3 ...`). **A bulk write resizes the array to the count provided** (verified: 50 → 25 → 50 round-trip on `MoveInPartNumbers`). | | **Array (bare reference)** | ❌ | ❌ | — | The plain `.` (no brackets) returns `MxCategoryCommunicationError, Detail=1003`. Always use `[]` for bulk operations. | | **Array element by index** | ✅ | ✅ | scalar of element type | Reference syntax `.[]`. **1-based**, runs from `[1]` to `[NumElements]`. `[0]` is invalid. | Legend: ✅ verified live, ⚠️ wiring present but no live instance to write, ❓ wiring present but no live instance found, ❌ not supported by MxAccess at this layer, – not applicable. To test write support for a type, use [`../../grdb/`](../../grdb/) to find a deployed instance whose `dynamic_attribute.mx_data_type` matches and whose `mx_attribute_category` is in `(2-11, 24)` ([Writeable_*](../../aot/dev-guide/appendix-f-attribute-categories.md) family). ## 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`. ## Reading arrays MxAccess accepts **two** reference forms for arrays — pick by what you need: ### Whole array — `.[]` (empty brackets) ```powershell mxa read 'MESReceiver_001.MoveInPartNumbers[]' --llm-json ``` Returns the full array as a single JSON value: ```jsonc { "tag": "MESReceiver_001.MoveInPartNumbers[]", "ok": true, "value": ["", "11111", "", "", /* ... 50 elements total ... */], "quality": 192, "statuses": [{"Success":-1,"Category":"MxCategoryOk", ...}] } ``` The array is fixed-length (sized at deploy time per the template's `array_dimension`). Empty string elements are unset slots, not gaps. ### Single element — `.[N]` ```powershell mxa read 'MESReceiver_001.MoveInPartNumbers[2]' --llm-json ``` Indices are **1-based**: `[1]` is the first element, `[NumElements]` is the last. `[0]` is invalid. Single-element reads are also writeable: `mxa write '.[N]' `. ### Whole array write — also via `[]` Pass one positional value per element after the tag. The CLI bundles them into a strongly-typed array (`string[]`, `int[]`, `bool[]`, …) before writing. ```powershell # Write a 50-element string array mxa write 'MESReceiver_001.MoveInPartNumbers[]' \ "" "11111" "" "" "" "" "" "" "" "" \ "" "" "" "" "" "15" "" "" "" "" \ "" "" "" "" "" "" "" "" "" "" \ "" "" "" "" "" "" "" "" "" "" \ "" "" "" "" "" "" "" "" "" "" # Write a typed array mxa write 'SomeObj.SomeFloats[]' 1.0 2.5 3.14 --type float ``` > ⚠️ **A bulk write resizes the array to the count provided.** If the configured `array_dimension` is 50 and you supply 25 values, after the write `mxa read '...[]'` returns **25** elements, not 50. The trailing slots are deallocated, not zero-filled. Always supply the full element count when you want to preserve the array's logical size — fetch the current count via `mxa read '...[]' --llm-json` first, or read it from `array_dimension` in [`../../grdb/queries/attributes.sql`](../../grdb/queries/attributes.sql). > > Mixing scalar / array forms is guarded: passing multiple values without `[]` exits 2 with a clear error message. ### What does *not* work ```powershell mxa read 'MESReceiver_001.MoveInPartNumbers' # bare ref, no brackets # → MxCategoryCommunicationError, Detail=1003 ``` The plain reference (no `[]`, no `[N]`) is rejected by the proxy on both read and write. Always include the brackets — empty for whole-array, indexed for element. ### Discovering array length The CLI doesn't (yet) auto-discover element count. Two ways to find it: 1. Read with `[]` and count the returned values (this is the **runtime** length, which may have been resized by a previous bulk write). 2. Query the Galaxy Repository's [`../../grdb/queries/attributes.sql`](../../grdb/queries/attributes.sql) — the `array_dimension` column reports the **configured** size from the template at deploy time. ## 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 `.`.