# MTConnect integration-test fixture — the official C++ Agent + an SHDR data source The MTConnect C++ Agent (`mtconnect/agent`, pinned) serving a canned device model, fed live data by a standard-library SHDR adapter. No image build step: both services run stock images with this folder's files bind-mounted. > **The published image is `mtconnect/agent`, not `mtconnect/cppagent`.** `cppagent` is the > name of the source project on GitHub; there is no Docker Hub repository under that name. | File | Purpose | |---|---| | [`docker-compose.yml`](docker-compose.yml) | Two services: `agent` (published on :5000) and `adapter` (internal, :7878) | | [`agent.cfg`](agent.cfg) | Agent configuration, bind-mounted at `/mtconnect/config/agent.cfg` (the image's CMD path) | | [`Devices.xml`](Devices.xml) | The canned device model the Agent serves from `/probe` | | [`adapter.py`](adapter.py) | SHDR feeder — the data source. Pure stdlib, runs on a stock `python:*-alpine` | ## Why there is an adapter service The `mtconnect/agent` image ships **only** the agent binary plus its schemas and styles — there is no bundled simulator. An Agent with no adapter answers `/probe` correctly and then reports **every observation `UNAVAILABLE` forever**, which cannot prove that reads return real values or that the `/sample` long poll delivers anything. `adapter.py` is what makes the fixture live. The Agent dials **out** to the adapter (see the `Adapters` block in `agent.cfg`); the adapter is not published to the host. ## Run From the shared Docker host (stack dir `/opt/otopcua-mtconnect`): ```bash docker compose up -d --wait docker compose logs -f agent docker compose down ``` From a dev box via the helper (see CLAUDE.md "Docker Workflow"): ```powershell lmxopcua-fix sync mtconnect # push this folder to /opt/otopcua-mtconnect/ lmxopcua-fix up mtconnect # single-service-shaped stack, no profile argument lmxopcua-fix logs mtconnect lmxopcua-fix down mtconnect ``` ### Running it on a Mac macOS **squats port 5000** — AirPlay Receiver (ControlCenter) binds `*:5000` and wins the race, so `docker port` reports a healthy publish while every request answers `403 Forbidden` with `Server: AirTunes`. Use the host-port override: ```bash MTCONNECT_AGENT_HOST_PORT=5555 docker compose up -d --wait MTCONNECT_AGENT_ENDPOINT=http://127.0.0.1:5555 dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests ``` ## Endpoint - Default: `http://10.100.0.35:5000` (the shared Docker host; 5000 is the Agent's own default port). - Override with `MTCONNECT_AGENT_ENDPOINT` to point at a real Agent on a machine tool. - `MTCONNECT_AGENT_HOST_PORT` changes only the **published host port** of the fixture container. `MTConnectAgentFixture` issues one `GET {endpoint}/probe` at collection init and records a `SkipReason` when it fails, so the suite skips cleanly on a box with no fixture running. ## The seeded device model Every named DataItem deliberately has `name != id` — the inverse of the driver's hand-authored unit fixtures, and the only arrangement under which confusing the browse name with the observation correlation key is visible. | DataItem `id` | `name` | Category | Type | Repr. | Inferred type | |---|---|---|---|---|---| | `fixture_avail` | `Favail` | EVENT | AVAILABILITY | | String | | `fixture_x_pos` | `Xact` | SAMPLE | POSITION (ACTUAL, MILLIMETER) | | Float64 — **moves** | | `fixture_x_load` | `Xload` | SAMPLE | LOAD (PERCENT) | | Float64 — **moves** | | `fixture_x_travel` | `Xtravel` | CONDITION | POSITION | | String + IsAlarm | | `fixture_c_speed` | `Cspeed` | SAMPLE | ROTARY_VELOCITY (REVOLUTION/MINUTE) | | Float64 — **moves** | | `fixture_c_temp_series` | `Ctemps` | SAMPLE | TEMPERATURE (CELSIUS) | TIME_SERIES | Float64 **array**, ArrayDim `null` | | `fixture_mode` | `Cmode` | EVENT | CONTROLLER_MODE | | String | | `fixture_execution` | `Pexec` | EVENT | EXECUTION | | String | | `fixture_partcount` | `Pcount` | EVENT | **PART_COUNT** | | **Int64** — the regression case | | `fixture_linenumber` | `Pline` | EVENT | LINE_NUMBER | | Int64 | | `fixture_program` | `Pprogram` | EVENT | PROGRAM | | String | | `fixture_block` | `Pblock` | EVENT | BLOCK | | String — **never fed ⇒ UNAVAILABLE** | | `fixture_varset` | `Pvars` | EVENT | VARIABLE | DATA_SET | String (structured ⇒ BadNotSupported) | | `fixture_logic` | `Plogic` | CONDITION | LOGIC_PROGRAM | | String + IsAlarm | | `fixture_asset_changed` / `fixture_asset_removed` | — | EVENT | ASSET_CHANGED / ASSET_REMOVED | | String | The Agent additionally injects **its own `` self-model device** (connection status, observation update rate, adapter URI). That is normal for every MTConnect 2.x Agent and the test suite excludes it from value-plane assertions — its update-rate samples tick whether or not any adapter is attached, so including them would let "the stream delivered a changed value" pass against an Agent with no data source at all. ## Two things a real Agent taught us (both cost a fixture restart to find) 1. **`agent.cfg` must be pure ASCII.** A single non-ASCII byte — even inside a comment — makes the config parser reject the whole file with a bare `Failed / Stopped at line: N` and exit. 2. **`sampleCount` is not a DataItem attribute.** It belongs to a TIME_SERIES *observation*. An Agent that meets it on a declaration logs `The following keys were present and not expected: sampleCount` followed by `DataItems: Invalid element 'DataItem'` and **drops the entire data item** from the device model. `MTConnectDataTypeInference` therefore only ever sees `sampleCount = null` from a real Agent, so a live TIME_SERIES tag is always a variable-length array.