pyippdme

A Python client and server package for I++ DME (Dimensional Measurement Equipment interface, VDMA 8722), the line-based TCP protocol used to control coordinate measuring machines (CMMs) and related equipment.

This is alpha software. It is not complete, and its interfaces are not stable and may change at any time.

This package has not been verified against real hardware. Commands sent to a real machine may cause unexpected motion, collisions, or damage. Use it with real equipment entirely at your own risk.

Quickstart: server

VirtualCMM is a fully simulated CMM with every built-in command class enabled and named coordinate systems persisted to disk:

import asyncio
from pyippdme import VirtualCMM

async def main() -> None:
    await VirtualCMM().serve_forever(host="127.0.0.1", port=1294)

asyncio.run(main())

From the command line: ippdme serve --host 127.0.0.1 --port 1294. See examples/virtual_cmm_server.py and examples/raw_data_handling.py in the repository.

A plain IppDmeServer registers no commands. Pass the command classes you want, for example the two every conformant server needs:

from pyippdme import IppDmeServer
from pyippdme.server.classes import register_dme_class, register_server_class

server = IppDmeServer(command_classes=[register_server_class, register_dme_class])

Quickstart: client

The low-level IppDmeClient sends/receives raw protocol nodes and mirrors the wire format directly:

import asyncio
from pyippdme import CommandName, IppDmeClient
from pyippdme.client import builders

async def main() -> None:
    client = await IppDmeClient.connect("127.0.0.1", 1294)
    await client.start_session()
    (version,) = await client.call(CommandName.GET_DME_VERSION)
    print(version)
    await client.call(CommandName.GO_TO, *builders.go_to(x=10))
    await client.end_session()
    await client.close()

asyncio.run(main())

CommandName (a StrEnum) names every command the built-in classes register, and ParameterName names the argument/response identifiers inside them (X in GoTo(X(10)), Center/IJK in GoToOnCircle(...), …) - a plain string like "GoTo"/"X" still works everywhere too, both are there so this layer’s own users don’t have to retype/risk mistyping one by hand. pyippdme.client.builders goes a step further for commands whose argument shape (not just names) is easy to get wrong - builders.go_to(x=10) builds the same NamedValue(...) tuple as the example above by hand; IppDmeMachine itself uses the same functions internally, so a raw-client user and this layer’s own typed methods build requests identically.

IppDmeMachine wraps the same connection with typed methods and return values instead, grouped the way VDMA 8722’s own object model groups them (.server, .dme, .cart_cmm, .tool, .found_tool, .tool_changer, .scanning, .form_tester, .mover, .rotary_table, .part, .raw_data, .feature_extraction)

  • see pyippdme.client.model for what each namespace covers:

import asyncio
from pyippdme import IppDmeMachine

async def main() -> None:
    machine = await IppDmeMachine.connect("127.0.0.1", 1294)
    await machine.start_session()
    await machine.cart_cmm.go_to(x=10, y=20)
    print(await machine.cart_cmm.get_position())
    async for point in machine.scanning.scan_on_line(
        (0, 0, 0), (10, 0, 0), (0, 0, 1), step_width=2.0
    ):
        print(point)
    await machine.end_session()
    await machine.close()

asyncio.run(main())

Anything IppDmeMachine doesn’t wrap is still reachable via machine.client.call(...) directly.

The namespaces cover the commands and properties of VDMA 8722, including the deprecated FeatureExtraction class. For example machine.server has the session, error and property commands, including clear_all_errors().

Moves and measurements take everything the standard allows as arguments:

from pyippdme.client.model import ToolAlignment

await machine.cart_cmm.go_to(x=10, r=90, alignment=ToolAlignment((0, 0, 1)), sync=True)
report = await machine.cart_cmm.pt_meas(x=1, y=2, z=3, ijk=(0, 0, 1))

A response made of named values, such as the result of pt_meas() or get(), is a Report: a single number is a float and several numbers are a tuple. report.number("X") and report.vector("IJK") give the typed value.

A scan yields (x, y, z) points. To get other values for each point, such as the quality or the surface direction, use machine.scanning.reporting(...):

scan = machine.scanning.reporting("X", "Y", "Z", "Q", "IJK")
async for point in scan.scan_on_line((0, 0, 0), (10, 0, 0), (0, 0, 1), step_width=2.0):
    print(point.number("Q"), point.vector("IJK"))

Each method sends its command as soon as it is called and returns a handle. Awaiting the handle waits until the command is done and gives its typed result. You can also check the acknowledgement first:

call = machine.cart_cmm.pt_meas(x=1, y=2, z=3)  # command sent
await call.acknowledged()                       # the server received it
report = await call                             # a Report

scan = machine.scanning.scan_on_line((0, 0, 0), (10, 0, 0), (0, 0, 1), step_width=2.0)
await scan.acknowledged()
async for point in scan:                        # points as they arrive
    print(point)

An Ack only means the server received the command. A command that then fails is reported after the Ack and raises IppDmeServerError when you await the handle, not from acknowledged().

The standard (5.4.3) does not let a client send a command before the previous one was acknowledged. The client therefore sends commands one after another, also when you start several at once; commands for prioritized execution (names ending in E, such as AbortE()) go out at once, so that they can get past a running command.

For the raw responses instead of the parsed result, await call.transaction() gives the Transaction of the command that was sent:

call = machine.cart_cmm.pt_meas(x=1, y=2, z=3)
transaction = await call.transaction()
raw = await transaction.wait_complete()         # tuple of DataPayload nodes

Events from the server

A server can tell the client that something happened without being asked (5.5.3): a key pressed on the jog box, a clearance point or a manual point, a tool changed or a property set at the machine, a tool collection opened. machine.server.events() yields them as KeyPress, ClearancePoint, ManualPoint, ToolChanged, PropertyChanged and ToolCollectionOpened (pyippdme.client.events), and anything else as UnknownEvent:

async for event in machine.server.events():
    if isinstance(event, KeyPress):
        print(event.key)

A VirtualCMM sends them with key_press(), clearance_point(), manual_point(), change_tool(), open_tool_collection() and set_property(). A custom IppDmeServer uses send_event() with the builders in pyippdme.server.builders.

Raw data

All three ways to get raw data (file, shared memory, binary socket) deliver the scanpoints binary format ESBF of Annex C.2. pyippdme.rawdata.formats reads and writes it and the older SBF (unpack_esbf, pack_esbf, unpack_sbf, pack_sbf), and pyippdme.rawdata.transfer.read_samples reads the stream of a binary socket.

In-process connections

Everything that opens or accepts a connection (IppDmeClient, IppDmeMachine, IppDmeServer, VirtualCMM, Spy) takes a network argument. The default is TCP. A MemoryNetwork keeps the connections inside the process, so a server and its clients can talk without opening a port, which is handy for tests and for simulation:

import asyncio
from pyippdme import IppDmeMachine, VirtualCMM
from pyippdme.protocol.network import MemoryNetwork

async def main() -> None:
    network = MemoryNetwork()
    server = VirtualCMM(network=network)
    port = await server.start()
    machine = await IppDmeMachine.connect("virtual", port, network=network)
    await machine.start_session()
    print(await machine.cart_cmm.get_position())
    await machine.close()
    await server.close()

asyncio.run(main())

A MemoryNetwork ignores host names and matches connections by port only. The network is a small protocol (open_connection and start_server, mirroring asyncio), so other implementations can be plugged in the same way.

Spy

Spy is a proxy between a client and a server that reports every line it relays (on_message). It only observes unless you give it an intercept function, which is awaited for each received line and returns what to send on instead: the same bytes to forward it unchanged, other bytes to alter it, None to drop it, or a list of lines to replace it with several. Lines are raw bytes ending in \r\n. ippdme spy never filters anything.

from pyippdme.spy import Spy, SpyDirection, SpyMessage

async def intercept(message: SpyMessage) -> bytes | None:
    if message.direction is SpyDirection.TO_SERVER and b"GoTo" in message.line:
        return None  # drop every GoTo
    return message.line

spy = Spy("192.168.1.50", 1294, intercept=intercept)

A dropped or altered command can leave the client waiting for a response the server never sends. The spy does not invent one.

Static command catalog

Building your own tooling on top of this package - a REPL/TUI completion engine, a GUI command palette, a linter for hand-written scripts - and want to know what commands and arguments exist without a live connection? pyippdme.simulation.catalog.BUILTIN_COMMANDS is a static, read-only catalog of every command this package’s built-in classes register, computed once at import time - no server instance, no connection, no network:

from pyippdme.simulation.catalog import BUILTIN_COMMANDS

info = BUILTIN_COMMANDS["GoTo"]
print([p.name for p in info.arguments])  # ['Positions', 'Sync'] - argument-name hints

This is deliberately distinct from a live server’s actual capabilities (GetSupportedCommands/GetSupportedArguments(CommandName), 6.4.1, over a real connection - the authoritative answer for that specific server, which may register only a subset of the built-in classes, or add its own proprietary commands BUILTIN_COMMANDS has no way to know about). build_command_catalog(command_classes) builds the same kind of catalog for a custom class subset, e.g. matching a particular IppDmeServer(command_classes=[...]) configuration.

Digital twin and simulator window

VirtualCMM stays the small, GUI-free simulation for the command line. For a simulation you can see, pyippdme.twin builds on the same protocol server and adds a physical model, and pyippdme.gui shows it in a Qt window. A client connects over TCP with the normal I++ DME protocol and gets the answers of that model.

pip install "pyippdme[gui]"          # PySide6 and OpenCASCADE (cadquery-ocp)
ippdme gui --start --sample part.step

In the window you can load a sample from a STEP, IGES, STL or BREP file, place it on the table or the rotary table (position, rotation, “rest on table”), add box and cylinder fixtures, switch machine presets, change the playback speed, the measuring noise and the part temperature, jog by hand, and watch the protocol lines. Measured points and scan point clouds appear in the 3D view.

What the twin simulates behind the protocol:

  • Motion. GoTo, Step and PtMeas take time with a trapezoidal speed profile (maximum vector speed and acceleration of the machine), AbortE() stops them, and the machine must be homed first (error 1011 otherwise). A target outside the machine volume is rejected with 1008.

  • Collisions. The stylus, probe holder, table, rotary table, the moving machine parts, the sample and fixtures are checked against each other with exact OpenCASCADE distances. A collision stops the machine where it hit and reports 2504. During PtMeas the tip is the sensor, so touching the part is not a collision.

  • Probing. PtMeas rays hit the exact B-rep of the CAD sample, with a measuring error that follows the machine’s MPE_E = A + L/K and MPE_P (a third of the MPE as one standard deviation) and a thermal expansion error for a part that is not at 20 °C.

  • Probing cycle. PtMeas follows Figure 29: move to the approach point, slow down to the probing speed without stopping, search at constant speed until the ball touches the surface (a tilted surface triggers later along the path), overtravel v²/2a, compensate the ball radius along the probing direction and retract at the GoTo speed. Nothing found within the search distance is error 1006. Speeds and accelerations come from GoToPar, PtMeasPar and ScanPar.

  • Scanning. ScanOnLine, ScanOnCircle and ScanOnHelix run at the scanning speed and follow the CAD surface. The five unknown-contour scans (ScanInPlaneEndIs..., ScanInCylEndIs..., Figures 35-39) trace the real surface in a scanning plane or cylinder until the stop sphere, plane or cylinder is reached; the algorithm is pyippdme.server.contour.trace_contour.

  • Optical sensors. LaserLine, LaserPoint, AreaScanner and Camera2D are sampled from a depth buffer of the placed CAD parts (pyippdme.twin.depthbuffer, pyippdme.twin.optical): depth gating, dropouts at steep incidence, triangulation shadows, flying pixels at edges and noise that grows with distance. Points can be raw scan lines (RSL), gridded (GSL) or averaged on a grid (QSP), and pyippdme.twin.pointtypes.qualified_edge_point builds the QEP of Figure 41. DataAcquire supports SingleShot, MultiShot and Sweep.

  • Tools and heads. Tools are data (pyippdme.twin.spec.ToolSpec, about 35 fields): head (fixed, indexing in 7.5° steps or continuous 5-axis), probe, extension, stylus, tip (including star tips) and measuring mode (touch, head_touch, scanning, laser, point_laser, area, camera). Every tool gets its protocol description (Tool.Id, the basic functions, so that a tool lacking one answers 2002). A tool must be qualified on a reference sphere (ReQualify) per tool and head position, or measurements carry a systematic error. ChangeTool drives to the tool rack and swaps the module; a crash of the breakaway module is error 1501. Tool.AlignmentVolume (a sphere, SPH and four numbers), Tool.CollisionVolume (boxes, OBB and 15 numbers) and Tool.AvrOffsets are answered from the tool geometry; the last two and Tool.Alignment need a calibrated (qualified) tool or answer 2000.

  • Tool collections. Tools can be organised in a tree (Figure 56, pyippdme.types.toolcollection): EnumToolCollection, OpenToolCollection and the other collection commands work on it, and entry names of an opened collection are accepted by ChangeTool, FindTool and SetTool.

  • Coordinate systems. pyippdme.types.csy.CsyContext applies the chain of Figure 12 (MachineCsy, RotaryTableCsy, PartCsy, … with the Euler angles of the standard, φ ∈ [0, 360], θ ∈ [0, 180], ψ ∈ [0, 360]). The twin converts every position and direction of a command into machine coordinates and back, and VirtualCMM re-expresses its position when the active CSY changes.

  • Safety. Air pressure (error 1000) and the emergency stop (1009, 1005) brake the machine and lose the reference, so the machine has to be homed again.

  • Check artefact. pyippdme.twin.artifact builds an ISO 10360 style artefact: reference sphere, ring gauge, pin, cone, step gauge, a stepped wedge, four balls, a dome and a groove. pyippdme.twin.features fits and compares what was measured, per measuring mode, against the nominal values and the machine’s MPE; pyippdme.twin.check.run_check_program measures it with a client in every mode.

The path and timing algorithms are plain functions you can use for your own simulation (pyippdme.twin.planning: profile_time, profile_position, plan_homing, plan_tool_change, plan_qualification, collision_steps, …; pyippdme.twin.toolmath: tool kinematics), and the transformation maths is in pyippdme.types.csy.

Machines from STEP files

The machine is a set of STEP bodies that move with the axes. Export the default machine to see the layout, edit or replace the files, and load the directory again:

from pyippdme.twin import MachineModel

MachineModel.default("bridge-900", rotary=True).export("my_cmm")   # machine.toml + STEP files
machine = MachineModel.from_directory("my_cmm")
[machine]
preset = "bridge-700"          # start from a preset, then override
travel = [900, 1200, 700]
max_speed = 520
acceleration = 1200
table_kind = "fixed"           # "moving-y": the bridge stands, the table carries Y
rack_side = "back"             # back, front, left or right edge of the table
rack_inset = 45                # mm from that edge of the travel range; negative is outside
# rack_origin = [100, 650, 10] # or the first port at an explicit position
[machine.accuracy]
a_um = 1.6
k = 350
[machine.rotary]
origin = [450, 600, -10]       # a rotary table, centre on its top
speed = 90

[[component]]
name = "table"
step = "granite.step"           # no step: the generated body is used
[[component]]
name = "bridge"
step = "bridge.step"
moves_with = ["y"]
[[component]]
name = "carriage"
step = "carriage.step"
moves_with = ["x", "y"]
[[component]]
name = "quill"
step = "quill.step"
moves_with = ["x", "y", "z"]
[[component]]
name = "turntable"
step = "turntable.step"
rotates = true

A single STEP assembly works as well (MachineModel.from_step, or Load machine…): its parts are recognised by name (table/granite, bridge/portal, carriage/slide, quill/spindle/ram, rotary/turntable). Travel ranges and the machine zero that you do not give are estimated from the geometry (machine.spec.derived lists what was estimated), so check them. Coordinates are machine coordinates: origin at the home position, Z up, the table surface 10 mm below zero.

With table_kind = "moving-y" (preset moving-table-600) the bridge stands still in the 3D view and the table, with the rack, rotary table, fixtures, parts and measured points on it, slides along Y. Measuring coordinates and collisions stay in the frame of the table, so nothing changes for the client. In a machine file, moves_with then names what moves in the world: the table and what stands on it carry y, the carriage x, the quill x and z. The tool rack stands at rack_side (a row of ports along that edge) or at rack_origin. The default positions are my choice, not taken from a manufacturer’s layout.

The presets give representative numbers for bridge machines (measuring range, MPE_E = A + L/K, axis speed and acceleration in the range of public manufacturer data sheets); they are not a model of one specific machine. Take the numbers of your machine from its data sheet.

Your own simulation in the window

The window shows anything that satisfies pyippdme.twin.view.SimulationView (a snapshot, a list of meshes with poses, and listeners). What answers the client is a set of seams of the protocol server, each a small Protocol that you can implement yourself and pass to IppDmeServer or VirtualCMM:

Seam

Used by

Without it

pyippdme.server.motion.MotionModel

every move: time, limits, collisions

moves are instant

pyippdme.server.surface.SampleSurface

PtMeas

the commanded point is reported

pyippdme.server.surface.RawSensor

DataAcquire

points are made up from the request

pyippdme.server.backend.MachineBackend

scanning commands

nominal path

DigitalTwin.create_server() shows how the twin wires them. pyippdme.twin.host.ServerHost runs a server on a background thread so that a GUI keeps its main thread.

Limits of the twin: the 3D view is a software renderer without OpenGL, so very large meshes are thinned while the camera moves; tools are registered in the process-wide pyippdme.simulation.classes.tool_class.TOOL_CATALOG while a twin server runs; the machine’s own temperature is not modelled.

Deviations and open points

Where this package departs from VDMA 8722:2024-04, or has to guess:

  • Without max_pending the server acknowledges every command at once. With it (5.4.3), the Ack of a command is delayed until fewer than that many commands are unfinished; AbortE drops the ones not yet acknowledged.

  • VirtualCMM does not require Home() before it moves, has no machine volume (error 2500) and no collisions (2504), and never reports 1003 or 1014 (neither does the twin). Every measuring tool accepts every measuring and scanning command, so the error 2002 (“Type of probe does not allow this operation”) never occurs.

  • VirtualCMM stores coordinate system transformations and only re-expresses its position when the active CSY changes; the twin applies the whole chain (Figure 12) to every coordinate. The chain order follows Figure 12, which the extracted text of the standard does not contain and the maintainer supplied; the Euler angle convention follows 6.5.1.

  • Tool.AlignmentVolume is answered as SPH and four numbers (Table 118), Tool.CollisionVolume as OBB and 15 numbers (Table 117). The extension E of a box is taken as the half-length along each axis, which Table 117 does not say. Tool collections are listed with the kinds Collection and Tool (Table 125), and paths join node names with . as in PartXYZ.Rear.

  • Tool.A(), B() and C() are valid only inside Get; written as commands they are answered with 0508 (Table 113). The standard has no OnReport command.

  • UseSmallestAngletoAlignTool is written both ways in the standard; both are accepted. With the flag set, an alignment that turns an angle by 180 degrees or more fails with 2500.

  • A scan starts with the implicit PtMeas of 6.13 (all PtMeasPar, Retract 0) on a server with a motion model: the machine moves to the approach position and probes the start point, so a scan over empty space fails with 1006. The twin also stops a scan with 2504 when the stylus or the probe body would touch the part; this check is not done for the unknown-contour scans.

  • The rotary table takes the shortest way to R(r); at exactly 180 degrees it turns in the positive direction.

  • GetXtdErrStatus() reports active errors as ActiveError() and Severity() data lines. Table 15 can also be read as asking for error responses.

  • GetChangeToolAction() answers Argument(Switch),X(0),Y(0),Z(0). The standard lists the action as an unnamed value, followed by named ones.

  • GetRawDataShaMem() sends offset and size as decimal numbers, although Table 101 calls them “hex coded”, and GetRawDataFile() sends the URL as a quoted string although Table 103 calls it a name.

  • Values that Tables 66, 70 and 32 call unnamed (ER, Q, IJKAct) are sent named in the response to PtMeas and Get, because the grammar cannot mix unnamed and named values in one response.

  • VirtualCMM reports one machine class, although 6.4.1 allows several.

  • The standard gives no examples for these encodings, so they are guesses: the pi,pj,pk, si,sj,sk and R() items of the ScanOnCurve format, the include/exclude flag of ROI, and the Acqs(..), ROIs(..), QEPs(S(..)) and GeoElem() arguments of FeatureExtract.

  • The deprecated FeatureExtraction class (Annex J.2) is simulated by the minimal server and the twin (ROI, FeatureExtract), with the guessed encodings above.

  • The twin raises faults on request (2501, 2502, 2503, 0503 and, from the tool changer, 1500), reports touches that are illegal (1001) or too hard (2001 for excessive force), checks that a tool can reach a requested alignment (1507, 2505), and compensates part and scale temperature (Part.Temperature, XpanCoefficient). The optional protocols AlignmentSolver and TemperatureProvider let a custom motion model do the same.

  • Not simulated: stylus CAD import, cameras attached to components, error 0507, and the PtMeasSelfCenter report format beyond what the standard text shows.

Command client window

ippdme client-gui opens a Qt window that connects to any server (or, with --virtual, to a virtual CMM it starts itself, with the same --twin, --machine and --sample options as ippdme client).

ippdme client-gui 127.0.0.1 --port 1294
ippdme client-gui --virtual

Icon buttons open dialogs that build the commands for you: Move (GoTo or Step), Point (PtMeas with the probing direction), Line scan and Circle / helix scan, Tools (list the tools, ChangeTool or SetTool), Speeds (GoToPar, PtMeasPar, ScanPar) and Coordinates (SetCoordSystem, SetCsyTransformation with a read-back, and saving, loading and deleting named systems), Optical (read the sensor, then DataAcquire and the point transfer; the points appear in a 3D point cloud view that can be copied as CSV or saved as .xyz). A dialog shows whether its run is still going, how long it took, or the server’s error. Every command of the protocol also has a form (the All commands panel, grouped by task), and there is a command line with history and a script runner.

A strip above the log follows the machine: lamps for connection, session, homed, user enabled and busy, the active coordinate system, the tool, and the position (from every response with X, Y and Z and a short poll). Errors show in the status bar with a button that sends ClearAllErrors(). Every command is numbered in the log (#7 > GoTo(...), #7 done in 0.35 s). The red Abort button (Esc) works during a move. Shortcuts: F5 connect, Ctrl+Shift+V virtual CMM, Ctrl+1..8 the dialogs, Ctrl+L the command line. The UX analysis lists what was found and changed in this window and in the simulator.

The window holds no protocol logic. pyippdme.client.host.ClientHost runs the connection on a thread (commands may overlap, so AbortE works during a move), pyippdme.client.recipes builds the command lines of the dialogs, pyippdme.client.commandform the forms of all commands, and pyippdme.client.optical reads a sensor and acquires points; use them to put another interface on the client.

3D rendering

On a desktop the 3D view draws with OpenGL: the meshes are uploaded to the graphics card once and drawn with their pose, so orbiting the camera and moving the machine stay smooth even with a heavy part (about 14 times faster than the software renderer in a test with 86,000 triangles and a CPU-only OpenGL; a real graphics card is faster still). It needs nothing beyond PySide6. If no OpenGL context can be made (a remote desktop, the offscreen platform used in CI, a driver that fails to compile the shaders) the view falls back to a software renderer that projects and sorts the triangles with numpy. View > OpenGL rendering switches between them, and the environment variable PYIPPDME_RENDERER (auto, gl or software) sets the default. The corner of the view names the renderer in use.

Coordinate systems

The Coordinates tab lists the chain of 6.5.1 (MachineCsy, …, PartCsy) with what the client placed (SetCsyTransformation, LoadCoordSystem) and marks the system the client works in (SetCoordSystem). The systems are drawn as triads with their names in the 3D view (the active one bold; a system that coincides with the machine system has no triad of its own), and a row picked in the table is drawn bold too. Show the position in chooses the system of the position above the view; a client works in its own, so this only changes the display.

Game controller

In the Teach-in tab, Game controller jogs the machine with a pad (any controller SDL2 knows). Install the extra with pip install pyippdme[gamepad]. The left stick moves X and Y, the right stick Z (the pace follows the step size), the bumpers halve and double the step, and the buttons send the jog box events: A picks a point, B sends a clearance point, X sends Done, Y sends F1. pyippdme.gui.gamepad.PadMapping holds the mapping without needing a controller, and any object with poll() can replace the SDL reader.

Building on the package

What to use for what, without reading the simulation code:

You want to

Use

talk to a server from code

IppDmeMachine (typed) or IppDmeClient (raw commands)

run commands typed or scripted, and show every step

pyippdme.client.interaction.run_command_line (events: Acked, Received, Completed, Failed, …)

put a client on a GUI thread

ClientHost (own thread, overlapping commands), recipes (command lines for moves, points, scans, tools, speeds), commandform (a form for every command; pass your own catalog), optical (sensor info, DataAcquire and the point transfer)

run any asyncio work off the GUI thread

LoopThread; ServerHost and ClientHost are built on it

write a server for a real machine

IppDmeServer with your own command classes, or VirtualCMM with your parts below

simulate part of a machine

the optional parts of VirtualCMM: MotionModel (moves, probing, homing, coordinate chain), SampleSurface or RawSensor (what is measured), ToolHandler (tool changes, qualification, volumes) and MachineBackend (scans). Optional extras are separate protocols: CalibrationAware, OffsetProvider and ContourBackend

transform coordinates

pyippdme.types.csy (CsyContext, CSY_CHAIN): Figure 12 and the Euler angles of the standard

plan paths and time

pyippdme.twin.planning, pyippdme.twin.toolmath (no CAD needed), and pyippdme.server.contour.trace_contour for unknown contours

model tools, sensors, check artefacts

pyippdme.twin.spec, .optical, .depthbuffer, .pointtypes, .features (numpy only; the OpenCASCADE parts are twin.cad, twin.tools, twin.machine, twin.twin)

examples/custom_server.py builds a small server from the four parts, and examples/client_without_gui.py drives a server with ClientHost, recipes and optical.

Interactive shell

In the simple shell (ippdme client), Tab completes the command name at the start of the line, and directly inside a command’s brackets its argument names (GoTo( then R, Sync, X, …). Further Tabs cycle through the matches in place (Shift+Tab goes back), with no list or popup. It uses the standard library’s readline, so it is on where that exists (Linux; macOS lists the matches instead). Ctrl+D quits, and the shell says so when it starts.

The shell (and the full-screen ippdme tui, which has the same dot commands and a dropdown for them) also has meta commands, which start with a dot and are not sent to the server: .help lists them, .cmds [group] lists the commands by task, .man <command> prints what a command does, its arguments with their types (* marks an optional one, bare values are written in order without their names), what it returns and an example, and .quit leaves (Ctrl+D does too). Tab completes the meta command names and the command after .man. The descriptions are written for this package and are not the text of the standard; read the standard for exact definitions.

pyippdme.client.reference.CommandReference holds the same information for other front ends: find(name) returns a CommandDoc (summary, signature, arguments with type, optional and description, return value, notes, example, group), groups(), search() and manual() give the rest, and describe() adds a command of your own. MetaCommands is the dot commands as a class (run(line) returns the text and whether to quit). The command client window shows the same page in its Help tab (F1).

ippdme client 127.0.0.1 1294

Type bare method calls (StartSession(), GoTo(X(10), Y(20)), …) and see the raw &/#/%/! responses as they arrive. This is deliberately a barebones tool - just enough to see whether a server is answering, not a full shell: no meta-commands, no tab-completion, no reconnecting mid-session (the connection is fixed for the whole run). Also stdlib only (no prompt_toolkit or similar), so pyippdme never pulls in an interactive-shell dependency for a user of the API only - line editing is just input() with readline-provided history on platforms that have it. Unsolicited (E0000) events print as they arrive throughout the session.

Don’t have a server running yet? ippdme client --virtual (and ippdme tui --virtual, the full-screen equivalent) starts an in-process VirtualCMM and connects to it automatically - no separate ippdme serve in another terminal needed. It’s torn down again when you quit.

Want to run a file of commands instead of typing them? ippdme client 127.0.0.1 1294 --file session.iscript runs every line non-interactively (same syntax as typed interactively; blank lines and # comments are allowed and copied through verbatim, and a leading wire tag like 00047 GoTo(...) is accepted and ignored) and prints the same Ack/Data/Done/Error output - this project’s own take on the NIST/I++ DME reference test suite’s .prg/.res file pairs; see pyippdme.cli.script for how and why the format differs. --file and --virtual combine too, for a quick “does my script still work” check against a throwaway simulated server.

client, serve, and spy all share one --session-log <file> flag that records every wire line sent/received, plus connect/disconnect, in a single unified format (timestamp, then >/< for traffic direction or CONNECTED/DISCONNECTED, then the raw tagged wire text) - see pyippdme.cli.session_log. They also share --commands-file <file>, which records just the commands actually run (tag-stripped, one per line), in exactly the syntax ippdme client --file accepts - so a session can be replayed later.

Extending the server

Add commands from other VDMA 8722 classes via server.registry.command(...).

For your own proprietary commands, VDMA 8722 6.1 requires a two-character company namespace prefix (e.g. a fictional company “XX” would add XXMyCommand(...)); use command_proprietary/register_proprietary instead of command/register so that convention is built and validated for you rather than typed by hand:

from pyippdme import IppDmeServer
from pyippdme.protocol.signature import DataType, Parameter
from pyippdme.server.classes import register_dme_class, register_server_class

server = IppDmeServer(command_classes=[register_server_class, register_dme_class])

@server.registry.command_proprietary(
    "XX", "MyCommand", arguments=(Parameter("Value", DataType.INT),)
)
async def _my_command(ctx, args):
    ...

The same two-character convention applies to properties and parameters (e.g. GoTo(XXMyParameter, ...)); build those names with pyippdme.proprietary_name("XX", "MyParameter") too.

arguments is optional (a command registered without it still works, GetSupportedArguments just can’t describe it) but works the same way for command/command_proprietary as it does for register/register_proprietary

  • pass it so GetSupportedArguments("XXMyCommand") can answer for real instead of falling back to “0506 Argument not supported”.