# 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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(...)`: ```python 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: ```python 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: ```python 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`: ```python 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: ```python 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. ```python 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: ```python 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. ```bash 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: ```python from pyippdme.twin import MachineModel MachineModel.default("bridge-900", rotary=True).export("my_cmm") # machine.toml + STEP files machine = MachineModel.from_directory("my_cmm") ``` ```toml [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`). ```bash 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](ux-analysis.md) 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 ` 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). ```bash 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 ` 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 `, 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: ```python 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". ```{toctree} :hidden: api ux-analysis ```