Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions configs/sim/axis/mtconnect/.gitignore
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

Comment thread
sliptonic marked this conversation as resolved.
Outdated
# 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
Comment thread
sliptonic marked this conversation as resolved.
Outdated
262 changes: 262 additions & 0 deletions configs/sim/axis/mtconnect/README.md
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.
94 changes: 94 additions & 0 deletions configs/sim/axis/mtconnect/contrib/ha.py
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)
Loading
Loading