Parser captures Entry/Cell content; the observation index merges /sample deltas into the /current baseline (resetTriggered replaces, removed deletes, UNAVAILABLE drops). Fixture gains a TABLE item and periodic DATA_SET/TABLE churn. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
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, notmtconnect/cppagent.cppagentis the name of the source project on GitHub; there is no Docker Hub repository under that name.
| File | Purpose |
|---|---|
docker-compose.yml |
Two services: agent (published on :5000) and adapter (internal, :7878) |
agent.cfg |
Agent configuration, bind-mounted at /mtconnect/config/agent.cfg (the image's CMD path) |
Devices.xml |
The canned device model the Agent serves from /probe |
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):
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"):
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:
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_ENDPOINTto point at a real Agent on a machine tool. MTCONNECT_AGENT_HOST_PORTchanges 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 — JSON object (#650); toolNumber/offsetX/offsetZ stable, cycle moves every ~5 s |
fixture_worktable |
Ptab |
EVENT | WORK_OFFSET | TABLE | String — JSON object of objects (#650); G54/G55 stable, G56 row moves every ~5 s |
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 <Agent> 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)
agent.cfgmust be pure ASCII. A single non-ASCII byte — even inside a comment — makes the config parser reject the whole file with a bareFailed / Stopped at line: Nand exit.sampleCountis not a DataItem attribute. It belongs to a TIME_SERIES observation. An Agent that meets it on a declaration logsThe following keys were present and not expected: sampleCountfollowed byDataItems: Invalid element 'DataItem'and drops the entire data item from the device model.MTConnectDataTypeInferencetherefore only ever seessampleCount = nullfrom a real Agent, so a live TIME_SERIES tag is always a variable-length array.