Import LinuxCNC source tree
This commit is contained in:
144
lib/python/mtc/agent.py
Normal file
144
lib/python/mtc/agent.py
Normal 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())
|
||||
Reference in New Issue
Block a user