Import LinuxCNC source tree

This commit is contained in:
Wang Dequan
2026-08-20 06:17:39 +08:00
commit 2c814634c3
9429 changed files with 10556013 additions and 0 deletions

144
lib/python/mtc/agent.py Normal file
View File

@@ -0,0 +1,144 @@
# 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
# Agent core: holds the machine model, the observation buffer and the live
# source, and produces the four MTConnect documents. Transport-agnostic so it
# can be driven by the embedded HTTP server (http_agent) or an MQTT publisher.
import os
import threading
from datetime import datetime, timezone
from .ini_reader import IniReader
from .observations import build_dataitems
from .device_model import DeviceConfig, probe_xml
from .streams import (ObservationBuffer, current_xml, sample_xml, assets_xml)
from .lcnc_source import LcncSource
from .hal_items import HalSource
from .models import build_models
from . import kinematics as kin
def now_iso():
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
def asset_dir():
"""Directory holding runtime data assets (the extension schema).
Installed / RIP layout: $EMC2_HOME/share/linuxcnc/mtconnect. Falls back to a
'mtconnect' dir beside this package for an uninstalled source checkout.
"""
home = os.environ.get("EMC2_HOME")
if home:
d = os.path.join(home, "share", "linuxcnc", "mtconnect")
if os.path.isdir(d):
return d
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "mtconnect")
class OutOfRange(Exception):
pass
class AgentState:
def __init__(self, ini_path, buffer_size=131072):
self.ini = IniReader(ini_path)
self.model = kin.build_model(self.ini)
self.config = DeviceConfig.from_ini(self.ini)
self.dataitems = build_dataitems(self.model, self.config)
self.buffer = ObservationBuffer(self.dataitems, max_size=buffer_size)
self.source = LcncSource(self.model, self.config)
self.hal_source = HalSource(self.config.hal_items)
self.models = build_models(self.ini, self.model, self.config)
self._assets = []
self._last_in_spindle = None
self._last_values = {}
self._lock = threading.Lock()
# -- data collection -----------------------------------------------------
def poll_once(self):
"""Poll LinuxCNC once and fold new values into the buffer."""
self.source.poll()
values = self.source.sample_values()
values.update(self.hal_source.sample_values()) # user HAL_ITEM pins
assets = self.source.tool_assets()
ts = now_iso()
with self._lock:
self._detect_asset_change(assets, values)
self.buffer.ingest(values, ts)
self._assets = assets
self._last_values = values
def latest_values(self):
"""Flat {dataitem_id: value} snapshot from the last poll (for HA)."""
with self._lock:
return dict(self._last_values)
def _detect_asset_change(self, assets, values):
in_spindle = values.get("toolnum")
if in_spindle is not None and in_spindle != self._last_in_spindle:
if in_spindle and in_spindle > 0:
values["assetchg"] = "tool-%d" % in_spindle
self._last_in_spindle = in_spindle
# -- document builders ---------------------------------------------------
def probe_document(self):
with self._lock:
asset_count = len(self._assets)
return probe_xml(self.model, self.config, models=self.models,
creation_time=now_iso(), asset_count=asset_count)
def extension_schema(self):
"""Return (bytes, content_type) for the LinuxCNC extension XSD, or None."""
path = os.path.join(asset_dir(), "mtconnect-linuxcnc-1.xsd")
if not os.path.isfile(path):
return None
with open(path, "rb") as fh:
return fh.read(), "application/xml"
def model_file(self, name):
"""Return (bytes, content_type) for a served mesh, or None."""
ref = self.models.served.get(name)
if ref is None:
return None
if name in self.models.generated: # auto-generated in memory
return self.models.generated[name].encode("utf-8"), ref.content_type
if ref.path and os.path.isfile(ref.path):
with open(ref.path, "rb") as fh:
return fh.read(), ref.content_type
return None
def current_document(self):
with self._lock:
return current_xml(self.buffer, self.config, now_iso())
def sample_document(self, from_seq=None, count=100):
with self._lock:
first, nxt = self.buffer.first_sequence, self.buffer.next_sequence
if from_seq is None:
from_seq = first
if from_seq < first or from_seq > nxt:
raise OutOfRange("from=%s out of range [%d,%d]"
% (from_seq, first, nxt))
return sample_xml(self.buffer, self.config, now_iso(), from_seq, count)
def assets_document(self):
with self._lock:
assets = list(self._assets)
return assets_xml(assets, self.config, now_iso())