-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Feature/mtconnect/agent #4318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sliptonic
wants to merge
5
commits into
LinuxCNC:master
Choose a base branch
from
sliptonic:feature/mtconnect/agent
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+4,805
−1
Open
Feature/mtconnect/agent #4318
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
3be0b80
mtconnect: native MTConnect agent for LinuxCNC
sliptonic 097e437
mtconnect: documentation
sliptonic 3d236f7
mtconnect: optional Home Assistant bridge (contrib)
sliptonic 523fa38
mtconnect: use LinuxCNC's INI parser directly
sliptonic 8cc8dc3
mtconnect: address PR review feedback
sliptonic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # LinuxCNC runtime state (created when the sim runs) | ||
| sim.var | ||
| sim.var.bak | ||
| position.txt | ||
|
|
||
| # Python bytecode | ||
| __pycache__/ | ||
| *.pyc | ||
|
|
||
| # 3D model assets — supply your own; never tracked. The example uses | ||
| # MODEL_AUTO (generated box geometry) and needs no mesh files. | ||
| models/*.stl | ||
| models/*.obj | ||
| models/*.gltf | ||
| models/*.glb | ||
| models/*.ply | ||
| models/*.step | ||
| models/*.stp | ||
| models/*.FCStd | ||
| models/*.FCStd1 | ||
|
sliptonic marked this conversation as resolved.
Outdated
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,262 @@ | ||
| # MTConnect agent for LinuxCNC — demo config | ||
|
|
||
| First-class MTConnect support for LinuxCNC: a userspace, non-realtime agent that | ||
| exposes machine **status**, a rich **kinematic description**, and **tool data** | ||
| over MTConnect. It ships an embedded HTTP agent (no external dependency) and can | ||
| optionally publish over the standard MTConnect **MQTT** binding. The `/probe` | ||
| response carries enough kinematic detail for an external tool (e.g. a FreeCAD | ||
| Path plugin) to auto-configure a machine. | ||
|
|
||
| This directory is a demo sim config. The agent itself installs with LinuxCNC | ||
| (`bin/mtconnect-agent`, package `lib/python/mtc`, assets under | ||
| `share/linuxcnc/mtconnect`). For the full reference see the `mtconnect-agent(1)` | ||
| man page and the `[MTCONNECT]` section of the INI configuration chapter. | ||
|
|
||
| ## Quick start | ||
|
|
||
| ```sh | ||
| # From this directory: | ||
| linuxcnc example.ini # launches the sim + the MTConnect agent | ||
|
|
||
| # In another terminal: | ||
| curl http://localhost:5000/probe # MTConnectDevices (structure + kinematics) | ||
| curl http://localhost:5000/current # latest value of every DataItem | ||
| curl http://localhost:5000/sample?from=1&count=100 | ||
| curl http://localhost:5000/assets # CuttingTool assets (tool table) | ||
| ``` | ||
|
|
||
| You can inspect the generated device model without launching LinuxCNC (after | ||
| sourcing `scripts/rip-environment` in a RIP build, or with LinuxCNC installed): | ||
|
|
||
| ```sh | ||
| mtconnect-agent --dump-probe example.ini | ||
| mtconnect-agent --dump-probe ../vismach/5axis/table-rotary-tilting/xyzac-trt.ini | ||
| ``` | ||
|
|
||
| ## Enabling it in your own config | ||
|
|
||
| Add a few lines to your INI. The device model is generated automatically from | ||
| `[TRAJ]`, `[KINS]`, `[AXIS_*]` and `[JOINT_*]`. | ||
|
|
||
| ```ini | ||
| [MTCONNECT] | ||
| ENABLE = 1 | ||
| DEVICE_NAME = my_mill | ||
| UUID = linuxcnc-my-mill-0001 | ||
| HTTP_PORT = 5000 | ||
| # HTTP_BIND defaults to 127.0.0.1 (loopback); set 0.0.0.0 to expose on the LAN. | ||
| # HTTP_BIND = 127.0.0.1 | ||
| # TRANSPORT: comma list of http, mqtt, shdr (no inline comments in INI values) | ||
| TRANSPORT = http | ||
| SAMPLE_HZ = 10 | ||
| # MQTT_BROKER = localhost | ||
| # MQTT_PORT = 1883 | ||
| # MQTT_PREFIX = MTConnect | ||
| ``` | ||
|
|
||
| Load the agent from a HAL file (see `mtconnect.hal`): | ||
|
|
||
| ``` | ||
| loadusr -W mtconnect-agent | ||
| ``` | ||
|
|
||
| `-W` waits until the component is ready; the INI comes from `$INI_FILE_NAME`. | ||
| Loading it in the HAL file (rather than `[APPLICATIONS]`) makes the status pins | ||
| (`active`, `connected`) available for linking. | ||
|
|
||
| ## What is exposed | ||
|
|
||
| | Endpoint | Content | | ||
| |------------|---------| | ||
| | `/probe` | Device structure: Controller/Path, Axes (Linear/Rotary + Motion), spindle, and the `x:Kinematics` extension | | ||
| | `/current` | Latest value of every DataItem (execution, mode, positions, spindle, feed, tool) | | ||
| | `/sample` | Sequence-numbered observation history (`?from=&count=`) | | ||
| | `/assets` | Tool table as `CuttingTool` assets (location, diameter, length offsets) | | ||
|
|
||
| ### LinuxCNC → MTConnect mapping (`linuxcnc.stat()`) | ||
|
|
||
| | MTConnect DataItem | Source | | ||
| |---|---| | ||
| | `EMERGENCY_STOP` | `task_state` | | ||
| | `CONTROLLER_MODE` | `task_mode` | | ||
| | `EXECUTION` | `interp_state`, `task_paused` | | ||
| | `PROGRAM`, `LINE_NUMBER` | `file`, `motion_line` (executing line, not interp read-ahead) | | ||
| | `PATH_FEEDRATE` (+ OVERRIDE) | `current_vel`, `feedrate` | | ||
| | `POSITION` / `ANGLE` (ACTUAL/COMMANDED) | `actual_position`, `position` | | ||
| | `ROTARY_VELOCITY`, `ROTARY_MODE`, `DIRECTION` | `spindle[0]` | | ||
| | `TOOL_NUMBER`, `TOOL_ASSET_ID` | `tool_in_spindle` | | ||
| | `ASSET_CHANGED` | tool change detection | | ||
| | `CuttingTool` assets | `tool_table` | | ||
|
|
||
| ## The kinematic description (`x:Kinematics`) | ||
|
|
||
| Standard MTConnect models axes as `Linear`/`Rotary` components with `Motion` | ||
| elements (`PRISMATIC`/`REVOLUTE`, direction vector). LinuxCNC specifics that do | ||
| not map cleanly are carried in a versioned extension namespace | ||
| `urn:linuxcnc:mtconnect:1` — the primary contract for auto-configuration: | ||
|
|
||
| ```xml | ||
| <x:Kinematics module="xyzac-trt-kins" coordinates="XYZAC" joints="5" | ||
| params="sparm=identityfirst"> | ||
| <x:JointMap> | ||
| <x:Joint number="0" kind="LINEAR" axis="X" min="-200" max="200" home="0"/> | ||
| <x:Joint number="3" kind="ANGULAR" axis="A" min="-100" max="50" home="0"/> | ||
| <x:Joint number="4" kind="ANGULAR" axis="C" min="-36000" max="36000" home="0"/> | ||
| </x:JointMap> | ||
| <x:Axis name="X" kind="LINEAR" vector="1 0 0" min="-200" max="200"/> | ||
| <x:Axis name="A" kind="ANGULAR" vector="1 0 0" min="-100" max="50"/> | ||
| <x:Axis name="C" kind="ANGULAR" vector="0 0 1" min="-36000" max="36000"/> | ||
| </x:Kinematics> | ||
| ``` | ||
|
|
||
| A FreeCAD plugin reads the standard `Axes`/`Motion` tree, or the compact | ||
| `x:Kinematics` block, to create/configure a machine: axis list and type, | ||
| travel limits, home positions, kinematics module/type, and the joint↔axis map. | ||
|
|
||
| ## Digital twin (solid models) | ||
|
|
||
| An MTConnect twin (e.g. the viewer at demo.mtconnect.org/twin) renders a machine | ||
| from `/probe` alone: geometry is **referenced, never streamed**. The device model | ||
| carries `<SolidModel href="...">` elements pointing to external mesh files, plus | ||
| `<CoordinateSystems>` and a `<Motion>` chain; the viewer fetches each mesh once | ||
| and animates it using the streamed positions. | ||
|
|
||
| The bundled viewer at `/twin` is **off by default**; enable it with | ||
| `ENABLE_TWIN = 1`. Its 3D rendering uses three.js from the distribution's | ||
| `libjs-three` package (a Debian *Recommends*), which the agent serves from | ||
| `/three/` — LinuxCNC vendors no third-party JavaScript, and the twin still works | ||
| fully offline once that package is installed. | ||
|
|
||
| ### Zero-config geometry (`MODEL_AUTO`) | ||
|
|
||
| ```ini | ||
| [MTCONNECT] | ||
| MODEL_AUTO = 1 | ||
| ``` | ||
|
|
||
| The agent generates simple placeholder **box** meshes for the base and each axis | ||
| straight from the travel limits and serves them from memory — any machine gets a | ||
| functional twin with **no mesh files**. This is what the example config uses. | ||
|
|
||
| ### Real geometry (per-link meshes) | ||
|
|
||
| Supply your own meshes (STL / OBJ / glTF) for fidelity: | ||
|
|
||
| ```ini | ||
| [MTCONNECT] | ||
| MODEL_DIR = models ; base dir for relative paths (default: INI dir) | ||
| MODEL_BASE = frame.stl ; static frame / column (device-level SolidModel) | ||
| MODEL_X = x_table.stl ; link that moves with X | ||
| MODEL_Y = y_saddle.stl | ||
| MODEL_Z = spindle.stl | ||
| MODEL_SPINDLE = spindle.stl | ||
| ``` | ||
|
|
||
| Author each mesh in **millimetres** (MTConnect's canonical unit — the SolidModel | ||
| element carries no per-mesh unit) in the machine frame at the all-axes-zero pose. | ||
| An explicit | ||
| `MODEL_<axis>` overrides the generated box for that link, so files and | ||
| `MODEL_AUTO` can be mixed. | ||
|
|
||
| ### Topology and direction (both modes) | ||
|
|
||
| These describe the mechanics and are **not** derivable from a trivkins INI: | ||
|
|
||
| ```ini | ||
| MODEL_CHAIN = Y X Z ; nesting order of the moving links (base -> tip) | ||
| MODEL_PARENT_Z = BASE ; branch: Z (quill/tool) hangs off the base, not X/Y | ||
| MODEL_INVERT = X Y ; work-carrying links move opposite the reported coord | ||
| ``` | ||
|
|
||
| The agent then serves each mesh at `GET /models/<name>`, emits a device-level | ||
| `<SolidModel>` for the base and a per-axis `<SolidModel>` in each axis's | ||
| `<Configuration>`, and emits `<CoordinateSystems>` (WORLD → MACHINE) plus a | ||
| `<Motion>` chain (`parentIdRef`) so the twin nests transforms correctly. | ||
|
|
||
| ## Using the official cppagent instead of the embedded agent | ||
|
|
||
| Put `shdr` in `TRANSPORT` and the agent acts as an SHDR adapter (default port | ||
| 7878), streaming `<ts>|id|value` lines to an external cppagent. Configure that | ||
| cppagent with a `Devices.xml`, which the generated `/probe` document doubles as: | ||
|
|
||
| ```sh | ||
| mtconnect-agent --dump-probe example.ini > Devices.xml | ||
| ``` | ||
|
|
||
| The `dataItemId`s in the SHDR stream match the ids in that `Devices.xml`. | ||
|
|
||
| ## MQTT | ||
|
|
||
| With `mqtt` in `TRANSPORT` and `python3-paho-mqtt` installed, the agent publishes | ||
| the standard (vendor-neutral) MTConnect MQTT topics: | ||
| `<prefix>/Probe/<uuid>` (retained), `<prefix>/Current/<uuid>`, | ||
| `<prefix>/Sample/<uuid>`, `<prefix>/Asset/<uuid>/<assetId>`. A consumer discovers | ||
| the whole device from the retained Probe topic. | ||
|
|
||
| **Home Assistant** is not part of the core agent (its MQTT Discovery format is | ||
| HA-specific). An optional bridge, `contrib/mtconnect-ha-bridge`, reuses the | ||
| device model to publish HA discovery; configure it with arguments so no HA key | ||
| touches the machine INI: | ||
|
|
||
| ``` | ||
| loadusr -W mtconnect-ha-bridge --broker=[HA]BROKER --username=[HA]USER --password=[HA]PASSWORD | ||
| ``` | ||
|
|
||
| ## Layout (installed) | ||
|
|
||
| ``` | ||
| src/hal/user_comps/mtconnect-agent.py entry point -> bin/mtconnect-agent | ||
| lib/python/mtc/ the agent package: | ||
| ini_reader.py INI access (linuxcnc.ini, with offline fallback) | ||
| kinematics.py build the kinematic model from the INI | ||
| observations.py shared DataItem registry (keeps probe and streams in sync) | ||
| device_model.py /probe MTConnectDevices document (+ CLI --dump-probe) | ||
| lcnc_source.py live status + tool table from linuxcnc.stat() | ||
| models.py solid-model config + served mesh registry (digital twin) | ||
| streams.py /current, /sample buffer + XML; /assets XML | ||
| agent.py transport-agnostic agent core (document builders) | ||
| http_agent.py embedded HTTP server | ||
| mqtt_agent.py optional MQTT publisher (vendor-neutral MTConnect binding) | ||
| share/linuxcnc/mtconnect/twin.html digital-twin viewer | ||
| share/linuxcnc/mtconnect/mtconnect-linuxcnc-1.xsd extension schema | ||
| configs/sim/axis/mtconnect/ this demo config + tests | ||
| contrib/mtconnect-ha-bridge opt-in Home Assistant MQTT-discovery bridge | ||
| contrib/ha.py HA discovery helper (used by the bridge) | ||
| ``` | ||
|
|
||
| ## Testing | ||
|
|
||
| ```sh | ||
| . scripts/rip-environment # so mtc is importable and bin/ is on PATH | ||
| python3 test_mtc.py # offline suite (no running LinuxCNC required) | ||
| python3 validate_schemas.py # validate the documents against the MTConnect XSDs | ||
| # (needs the 'xmlschema' package) | ||
| ``` | ||
|
|
||
| `test_mtc.py` covers kinematics mapping, probe↔registry consistency (3- and | ||
| 5-axis), the observation buffer + streams, assets, the live HTTP endpoints, and | ||
| (when `xmlschema` is available) schema validation of all four documents. | ||
|
|
||
| ## Custom HAL pins | ||
|
|
||
| Expose any HAL pin/signal as a data item with `HAL_ITEM` lines (read via | ||
| `hal.get_value`, no HAL wiring): | ||
|
|
||
| ```ini | ||
| [MTCONNECT] | ||
| HAL_ITEM = pin=spindle.0.load, id=spindle_load, type=LOAD, units=PERCENT, component=spindle | ||
| HAL_ITEM = pin=hm2_5i25.temp, id=board_temp, type=TEMPERATURE, units=CELSIUS | ||
| ``` | ||
|
|
||
| Standard MTConnect SAMPLE types only (LOAD, TEMPERATURE, PRESSURE, VOLTAGE, | ||
| AMPERAGE, FREQUENCY, ANGLE, VELOCITY, TORQUE, …). `component=` hosts it under a | ||
| generic `Sensor` (default) or an existing component (spindle/controller/path/an | ||
| axis). Unsupported types are skipped with a warning. See `mtconnect-agent(1)`. | ||
|
|
||
| ## Status | ||
|
|
||
| Complete and packaged for the build: the HTTP transport, HAL component and | ||
| schema-valid documents; the vendor-neutral MQTT binding plus the optional | ||
| Home Assistant bridge contrib; the opt-in offline digital twin (three.js from | ||
| the distribution's `libjs-three`); and the SHDR adapter for an external | ||
| cppagent. Realtime servo-rate data is explicitly out of scope. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| # Copyright (C) 2026 LinuxCNC contributors | ||
| # | ||
| # This program is free software; you can redistribute it and/or modify | ||
| # it under the terms of the GNU General Public License as published by | ||
| # the Free Software Foundation; either version 2 of the License, or | ||
| # (at your option) any later version. | ||
| # | ||
| # This program is distributed in the hope that it will be useful, | ||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| # GNU General Public License for more details. | ||
| # | ||
| # You should have received a copy of the GNU General Public License | ||
| # along with this program; if not, write to the Free Software | ||
| # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | ||
|
|
||
| # Home Assistant MQTT Discovery helper for the mtconnect-ha-bridge contrib. | ||
| # | ||
| # Home Assistant's MQTT Discovery is an HA-specific convention (its topic layout, | ||
| # JSON payload schema and Jinja value_template are defined by Home Assistant, not | ||
| # an open standard), so it is NOT part of the core mtconnect-agent. This helper | ||
| # builds, for the optional bridge: | ||
| # * retained discovery configs under <ha_prefix>/sensor/<node>/<key>/config so | ||
| # HA auto-creates a Device with one sensor per value (no YAML needed), and | ||
| # * a small flat JSON state document the sensors read via value_json. | ||
|
|
||
| import json | ||
|
|
||
| _LIN_UNIT = {"MILLIMETER": "mm", "INCH": "in", "CENTIMETER": "cm"} | ||
|
|
||
|
|
||
| def _lin(config): | ||
| return _LIN_UNIT.get(config.linear_units, "mm") | ||
|
|
||
|
|
||
| def build_sensors(model, config): | ||
| """Curated, demo-friendly sensor set derived from the machine model.""" | ||
| lin = _lin(config) | ||
| sensors = [ | ||
| {"key": "execution", "name": "Execution", "icon": "mdi:cog-play"}, | ||
| {"key": "mode", "name": "Mode", "icon": "mdi:tune"}, | ||
| {"key": "estop", "name": "E-Stop", "icon": "mdi:alert-octagon"}, | ||
| {"key": "program", "name": "Program", "icon": "mdi:file-document-outline"}, | ||
| {"key": "toolnum", "name": "Tool", "icon": "mdi:screwdriver"}, | ||
| {"key": "pathfeed", "name": "Feed rate", "unit": lin + "/s", | ||
| "icon": "mdi:speedometer", "num": True}, | ||
| {"key": "spdl_speed", "name": "Spindle", "unit": "RPM", | ||
| "icon": "mdi:fan", "num": True}, | ||
| ] | ||
| for a in model.axes: | ||
| low = a.letter.lower() | ||
| icon = "mdi:axis-%s-arrow" % low if low in ("x", "y", "z") else "mdi:axis-arrow" | ||
| sensors.append({"key": "pos_%s" % low, "name": "%s position" % a.letter, | ||
| "unit": lin if a.kind == "LINEAR" else "°", | ||
| "icon": icon, "num": True}) | ||
| return sensors | ||
|
|
||
|
|
||
| def node_id(config): | ||
| return "".join(c if (c.isalnum() or c in "-_") else "_" for c in config.uuid) | ||
|
|
||
|
|
||
| def discovery_payload(sensor, config, state_topic, avail_topic): | ||
| payload = { | ||
| "name": sensor["name"], | ||
| "unique_id": "%s_%s" % (config.uuid, sensor["key"]), | ||
| "state_topic": state_topic, | ||
| "value_template": "{{ value_json.%s }}" % sensor["key"], | ||
| "availability_topic": avail_topic, | ||
| "device": { | ||
| "identifiers": [config.uuid], | ||
| "name": config.name, | ||
| "manufacturer": "LinuxCNC", | ||
| "model": "MTConnect", | ||
| }, | ||
| } | ||
| if sensor.get("icon"): | ||
| payload["icon"] = sensor["icon"] | ||
| if sensor.get("unit"): | ||
| payload["unit_of_measurement"] = sensor["unit"] | ||
| if sensor.get("num"): | ||
| payload["state_class"] = "measurement" | ||
| return payload | ||
|
|
||
|
|
||
| def state_json(values, sensors): | ||
| """Flat JSON of the curated keys; skip missing/UNAVAILABLE/structured.""" | ||
| out = {} | ||
| for s in sensors: | ||
| v = values.get(s["key"]) | ||
| if v is None or v == "UNAVAILABLE" or isinstance(v, (dict, list)): | ||
| continue | ||
| out[s["key"]] = v | ||
| return json.dumps(out) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.