Table of Contents

Workflow: Constructing a Project through the Web API

Basic Simulation describes what a project contains. This page describes how to build one so that it is actually finished — the ordering that avoids rework, how to shape the mission, and the acceptance test that tells you the project is complete rather than merely populated.

It is written for whoever assembles a .hincproj from a customer's deliverables — a person driving the browser UI, or an agent driving the same operations over the HTTP API. Where a step has an API form, the route sits with the step. Replaying and accepting the built project over the API is Replay Acceptance over the HTTP API.

flowchart TD
    New["1 · New project<br>(self-contained root)"]
    Assets["2 · Place assets<br>NC, STL, resource files"]
    Equip["3 · Equipment<br>chain, spindle"]
    Job["4 · Job<br>stock, ideal geom, material"]
    Ctrl["5 · Controller<br>brand + its own tables"]
    Tools["6 · Tool house"]
    Mission["7 · Mission<br>grouped, per-group resolution"]
    Replay["8 · Replay and read<br>every message"]
    Cross["9 · Numeric cross-checks"]
    Assume["10 · Record every assumption"]

    New --> Assets --> Equip --> Job --> Ctrl --> Tools --> Mission --> Replay --> Cross --> Assume
    Replay -->|any warning| Ctrl

Build through the API, not by editing the XML

Create and modify projects through the service's HTTP API. It is the same code path the UI uses, so a project built that way is on the supported create/load/save route. Hand-authoring the .hincproj XML is a fallback for fields the API cannot yet reach — and when you hit one, the better fix is to add the endpoint.

The sign-in, the reply envelope, the file roots, the project lifecycle and the key-based editing pattern the steps below rely on are Driving the Web Service over HTTP, which also lists the request bodies the framework's own OpenAPI document gets wrong. Three of its rules decide whether a scripted build can be trusted: check success in every reply rather than only the HTTP status; after editing inside a geometry or transformer through its key, re-install that slot before the next run; and do not drive an instance someone else is using.

Which endpoints the run reads

A project carries two controller models: the runner the simulation plays, and a legacy model saved beside it. Several of their routes look alike, and a value written to the legacy one is accepted, saved and read back without complaint while the run never sees it. Write machine data through these:

Datum Route the run reads Look-alike the run ignores
Machine chain POST /api/mech/machine-tool/load — see below —
Spindle capability POST /api/mech/spindle-capability/load-file {rootName, relFile} —
Controller brand POST /api/mech/soft-nc-runner/brand {Brand, CopyWorkCoordinates} PUT /api/Controller/cnc-brand
Travel X, Y, Z PUT /api/Controller/stroke-limit-xyz with exactly six numbers, [minX, maxX, minY, maxY, minZ, maxZ] in mm, which writes both models — any other count answers 400 and writes nothing; or PUT /api/mech/soft-nc-runner/machine-limits/{axis} {Positive, Negative} —
Stroke-limit check PUT /api/Controller/stroke-limit-check true or false — a project flag outside both models, saved with the project and on by default. Off stops the check of each played step (Stroke-Limit--Over) and the session-begin StrokeLimit--Unconfigured warning; the position each NC block ends at is still compared with any limit set (StrokeLimit--Positive, StrokeLimit--Negative), but a motion that leaves the travel mid-block, such as a drilling cycle's plunge or an arc, then goes unreported —
Rapid rate PUT /api/mech/soft-nc-runner/rapid-feedrates/{axis} {Value}, mm/min (deg/min on a rotary axis) PUT /api/Controller/rapid-feedrate
Home / G28 reference PUT /api/mech/soft-nc-runner/home-positions/{axis} {Value} —
Tool-change position PUT /api/mech/soft-nc-runner/tool-change/{axis} {Value}, or {Stay: true} to leave the axis where it is —
Tool-change time PUT /api/mech/soft-nc-runner/tool-change/tooling-time {Value}, seconds PUT /api/Controller/tooling-time
Maximum spindle speed PUT /api/mech/soft-nc-runner/controller-params/max-spindle-speed {Value}, rpm — recorded, not read (§4) —
Subprogram folders PUT /api/mech/soft-nc-runner/subprogram-folders {InternalFolder, ExternalFolder}, relative to the project folder —

The 400 that stroke-limit-xyz gives a wrong count names the shape it expects and the count it got — for five numbers, Expected 6 values [minX, maxX, minY, maxY, minZ, maxZ] in mm; got 5. A missing body or a JSON null is refused before the route runs: the framework's own validation answers it with a 400 whose body is a problem-details JSON object rather than that message.

The four per-axis routes — machine-limits/{axis}, rapid-feedrates/{axis}, home-positions/{axis} and tool-change/{axis} — take as {axis} one of the names their own GET lists, in any letter case, and write under the listed spelling, so machine-limits/x sets X. The names listed are the brand preset's seeded X, Y and Z plus the axes the machine tool's chain adds (§2); these routes add none. Any other name is refused with HTTP 200, "success": false, the listed names in axes and a message that names it — No axis 'Q' on this machine; its axes are X, Y, Z, B, C., or No axis 'Q' on this machine; it has no axes yet. when nothing is listed — and nothing is written. A name that matches several listed axes only when letter case is ignored is refused the same way rather than guessed at: No axis 'ax' on this machine: several axes (AX, Ax) match it only ignoring letter case, so none is taken; its axes are ….

The same split runs through the program-data tables: the work offsets and tool offsets a run resolves are the runner's, under /api/mech/soft-nc-runner/ (§4). Read every value back with the GET of the same route after a save and a reload.

Loading the machine chain

From a file, the chain loads through POST /api/mech/machine-tool/load with {"RootName": "ProjectDirectory", "RelFile": "MachineTool/<name>.mt"}. RootName is ProjectDirectory or ResourceDir, and any other root is refused with 400. A chain built elsewhere therefore goes into the project folder first — uploaded through the file routes — and loads from ProjectDirectory. A chain picked under ResourceDir is written into the project at the same relative path, side files included, and referenced there. (POST /api/mech/machine-tool/update installs a chain already in the object store, and …/create a blank cutter-location device; neither reads a file.)

The route answers HTTP 200 whether or not the load worked: a missing file or a chain the loader refuses comes back as "success": false with the reason in message. On success the body names the chain's typeName and the relFile the project records, and GET /api/mech/machine-tool reports the same afterwards. The load's reply does not carry the loader's messages: an end anchor the machine lacks or carries in the wrong letter case is reported to the service log (GET /api/Project/logs), and in the messages of the next POST /api/Project/reload once the project is saved. A machine without both end anchors cannot run. A machine the resource library does not carry can be built with no shapes — A machine with no model.

1. One self-contained root

The .hincproj and every asset it references live under one folder — the project's own directory — and every reference is relative to it with no ../.

<project>/
├── <name>.hincproj
├── NC/                     the programs the mission plays
├── Delivered/              the customer's files, exactly as received
├── Geom/                   workpiece / fixture STL
├── MachineTool/            the kinematic chain
├── SpindleCapability/
├── WorkpieceMaterial/
├── CuttingParameter/
├── CutterMaterial/
└── README.md               what was assumed, and why

Two things do not move themselves, so bring them in by hand before you wire anything:

  • NC files. A program-file command stores a plain relative path; nothing copies the file.
  • Machine-tool side files. A chain that externalises its mechanism references the side file and its STL bodies by bare name, relative to the .mt's own folder — keep the whole folder together.

Resource files (chain, spindle, cutting parameters, workpiece material, cutter material) are pre-prepared data loaded by reference. Loading one from the Resource root writes the project's own copy at the same sub-path — WorkpieceMaterial/<name>, say — and records that project-relative path, so the saved project never points into the shared library; a file already inside the project is referenced where it is. Uploading the file into the project first and loading it from the Project root comes to the same thing, and works on every version: check after each library pick that the copy landed in the project folder, and upload it yourself where it did not (Files and Named Roots).

Programs: keep the delivery, play a copy

Keep every program exactly as the customer delivered it, under Delivered/, and play copies from NC/. Three kinds of change are made, and only to the copies:

  • Names the call lookup needs. A subprogram or macro called by number is found only under a fixed set of file names in the subprogram folder — O0025.NC or O25.NC for a P25 call, among others; see General NC Code Support. A callee delivered as O25.dat is copied under one of those names with its content untouched. A callee that is not found is reported as an error, the call is skipped, and the run still finishes — with nothing cut when all the cutting lives in the callee.
  • Constructs that would change the result. When a replay shows the reader not honouring a construct the result depends on — a G code chosen by a variable (G[#120]), a macro argument written as a variable (G65 P25 A#120), a reference return whose axis words sit behind another G code in the block (G91 G28 G0 Z0) — rewrite that line in the copy into the literal form the machine executes (G55 for a G[#120] holding 55; G65 P25 A55; G91 G28 Z0 when that block returns Z home) and leave every other line as delivered.
  • What the operator does by hand. A program written for a machine whose operator changes the tool by hand has no T… M06, and nothing mounts a tool but a tool-change command or a Manual Tooling entry, so the copy on its own would play with the spindle empty and build no machining step (Step-Tool--NotEquipped). Keep the copy without the change, and put a Manual Tooling entry naming the tool above the program's Program File in the mission: it mounts the tool as the operator does, builds no step and takes no file number (Mounting the tool the operator loads by hand; its route is in the table of §3). The tool's length is still the program's: when it applies no length compensation, give the copy a G43 H1 before its first move down, unless the length is folded into the work offset's Z instead, as the shop does (§4).

Open each rewritten copy with a comment naming what was changed, and list the same changes in the project's README with the reason for each, so the copy can be compared with the delivery line by line. When a release reads a construct natively, retire the rewrite and play the delivered line.

Constructs that only raise a known message without changing the motion — a named program header, a smoothing or look-ahead code, a cycle-time timer read — can stay as delivered. List them in the README as expected messages, by id and count per play, so the next replay is checked against that list instead of being investigated again.

2. Order the build to avoid rework

Later steps read earlier ones, so this order costs the least:

  1. New project at its final path. It is not empty — see §3.

  2. Machine tool, then spindle capability.

  3. Workpiece: stock geometry, ideal (target) geometry, resolution, the transformers of the workpiece branches to its Program-Zero Anchor and its Fixture Mount, then material and cutting parameters. A material is used as its file writes it: one whose HeatConductivity_WdmK (W/(m·K)), Density_gdcm3 (g/cm³) or HeatCapacity_JdgK (J/(g·K)) is not a finite positive number, or lies outside the range of a solid — a density typed in kg/m³, say — comes back in the load's messages and is named again when a session or a play begins (Material-ThermalConstants--NotFinitePositive, Material-ThermalConstants--OutOfRange). One whose file lacks the element is not given, and no default stands in: the thermal model is skipped for every tool, with the temperatures left at ambient and the wear at zero, and the material is named at the same places (Material-ThermalConstants--NotGiven). When the part arrives as a mesh exported from CAM rather than as labelled stock and design CAD, first decide what the mesh is and where program zero sits in it — Building the Workpiece from a Reference Mesh, which also gives the API calls for an STL slot.

  4. Controller: brand first — switching brand later resets brand-specific tables — then the controller's own tables (§4).

  5. Tool house, per tool, in this order — each step reads the one before:

    1. Create, then number. A new tool takes one past the largest number in the house; rename it to the T number the program calls (POST /api/ToolHouse/CreateTool, then POST /api/ToolHouse/RenameTool?toolId=…&newId=…). The number is also the offset row the H and D words read once the offset table is refreshed.
    2. Shaper profile (PUT /api/Cutter/{id}/shaper-profile), e.g. {"AptType": "BallApt", "Diameter_mm": 4, "FluteHeight_mm": 5}; AptType is one of ColumnApt, ConeApt, BallApt, TaperApt and GeneralApt, case-insensitive and with or without the Apt. A ball profile takes only the diameter and the length of cut. The cutter routes create the tool's milling cutter on first use, so no separate call is needed for it.
    3. Fluting (PUT /api/Cutter/{id}/fluting), after the profile: the edges are built against the profile of that moment, so re-send the fluting after any profile change. It is replaced whole: "Type": "uniform" is required — anything else is refused with 400 — and every other field left out takes the route's own default — 2 flutes, helix 30°, radial rake 0°, radial relief 5°, flute offset angle 0°. A rake of 0° is not neutral, since the rake orients the force each edge point produces; choose one from the work material when the tool sheet gives none (Cutter Geometry). Send every value, including a flute offset angle entered earlier, and the side contour itself ("SideContour": {"Type": "consthelix", …}): a fluting without one, or with a type the route does not recognise, is accepted and leaves the flutes with no side edge.
    4. Exposed cutter height — the stick-out (PUT /api/ToolHouse/{id}/exposed-height). A new tool starts at 0, with the cutter hidden inside the holder.
    5. Setup angle — how far the cutter is turned in the holder (PUT /api/ToolHouse/{id}/setup-angle, the body a bare number of degrees). It defaults to 0 and only the milling physics reads it; enter it when the flutes' angular position matters, as it does for a smart holder reporting in the sensor's own frame (Smart Tool Holder). It is a property of the clamping, so it survives a fluting re-send.
    6. Holder. List the library first (GET /api/file-explorer/list?rootName=ResourceDir&relativePath=Holder): a .Holder file there carries a finished profile. No route loads one into a tool, so read it with GET /api/file-explorer/read-text and send its Z–R pairs whole as below. Set its type (POST /api/ToolHouse/SetHolderType), read its profile key (POST /api/CylindroidHolder/Get with a session key answers it in cylindroidKey, as <sessionKey>-HolderCylindroid), replace the whole profile with POST /api/Cylindroid/UpdateAllPairs — a new cylindroid holder's profile is a Ø6 placeholder from Z 40 to Z 80 that adds no length until the holder re-derives it — then POST /api/CylindroidHolder/UpdateGeometryContent so the holder re-derives its length. The profile's frame is in Cutter Geometry.
    7. Upper beam. Create it through the cutter (POST /api/Cutter/{id}/upper-beam/create?kind=Cylindroid&key=… — the new cylindroid is stored under the key you pass), write the pairs with POST /api/Cylindroid/UpdateAllPairs on that key — measured from the cutter tip, the first at the flute top and the last at the overall length (Cutter Geometry) — then POST /api/Cutter/{id}/upper-beam/resync and read the warnings it returns; an empty list is the pass.
    8. Cutter material, and coating layers if the tool has any. A tool without a flute material runs no thermal physics and reports no yield-stress ratio (Tool-FluteMaterial--Missing). Flute, shank and coating materials are checked as the workpiece's is: the load's messages and the tool change name a thermal constant that is not a finite positive number or lies outside the range of a solid, and one whose element the file lacks: not given, with no default standing in, it leaves the tool without a thermal step, as a missing flute material does (Material-ThermalConstants--NotGiven); a cutter material whose file lacks its hardness or wear-coefficient curve (TemperatureVsHardnessCurve, TemperatureVsWearCoefficientCurve) is named the same way where its curves are read — as the flute material or a coating layer's, never as the shank's (Material-WearCurves--NotGiven): the native thermal model does not run for the tool.
    9. Smart-holder observation point, when the tool carries a sensor. A new tool points at the holder nose with a height of 0; see Smart Tool Holder.

    Then refresh the offset tables from the tool house, and check each tool-length row against the holder's gauge length plus the stick-out. GET /api/ToolHouse/{id} returns the stored fluting, profile and clamping values — setupAngle_deg among them — for a last read-back.

  6. Program zero: with the workpiece placed and the chain loaded, align the two in the direction the job calls for — Program Zero Alignment. When the customer supplied recorded offsets, keep them and move the part onto them (§4); derive a row from the model only when planning a job that has no recorded offset. When several parts share the table, only the row of the part carrying the program-zero anchor can come from the model: the rows of the others are that row plus the part spacing.

  7. Mission (§3).

  8. Save, then load the saved file back and re-check. A project that cannot be reloaded cleanly is not built.

Important

Verify the axis set after loading a machine tool. Each axis is a role of the machine tool (AxisX … AxisC) bound to one of its branches; a chain file binds them by branch name (X/Y/Z/A/B/C), so a branch left at its authoring default name binds no axis, and that axis silently does not exist. The controller's axis list cannot show this for a linear axis — every brand preset already declares X, Y and Z, and a chain only adds axes — so read the roles: GET /api/mech/equipment-topology/roles/machine-tool lists each axis with the branch it is bound to and its checks (an unbound axis is simply unbound; AxisTransformerMismatch names a branch of the wrong kind). Every axis needs a branch carrying a DynamicTranslation (X, Y, Z) or a DynamicRotation (A, B, C); the chain itself is POST /api/mech/machine-tool/xml. The spindle (Spindle) is not an axis and is found by topology, not by name: the branch arriving where the tool mount hangs from, when it carries a DynamicRotation about the tool mount's −Z through its origin. A machine without one plays as before, its spindle simply not turned on screen; a rotation off the tool axis is flagged SpindleOffToolAxis.

3. Shape the mission around the process, not the file list

A new project arrives with a mission already in it: the five Setup commands, two Program File entries naming the placeholders NC/NC-File-Name-1.nc and NC/NC-File-Name-2.nc, and a Post-Execution with every output off. Each placeholder raises the error RunNcFile--NotFound on every play and is skipped, and an entry added through POST /api/Mission/list-command/entries without an insert index lands after them — below the Post-Execution, which writes only what has played above it, so nothing that entry plays reaches its outputs; with nothing played above it either, its writers end on StepFile-Write--NoFile and ShotFile-Write--NoFile (for the outputs switched on). Pass "InsertIndex" with the Post-Execution's current index to put a program above it — 5 once the two placeholders are deleted — or move the Post-Execution to the end afterwards with POST /api/Mission/list-command/entries/{path}/move and {"Direction": "to", "TargetIndex": <last index>}. Edit the seeded entries, or delete them with DELETE /api/Mission/list-command/entries/{path} from the last index down — the paths after a deleted entry renumber — and read the result back with GET /api/Mission/list-command/entries.

A flat list of program files runs correctly but reads badly and gives you one knob for the whole job. Group the programs into one List command per operation family, in the order the process actually runs:

Machining Motion Resolution
Collision Detection      [Off]
Pause on Failure         [Off]
Physics                  [On]
Record Meshed Geometry   Read On First Or Write · Cache/0-stock.wct         (STL stock only)
▸ 1 · Drilling                    Machining Resolution 0.5 mm
      centre drill, drill
      Record Meshed Geometry      Read On First Or Write · Cache/1-drilling-done.wct
▸ 2 · Outside roughing            Machining Resolution 0.5 mm
      roughing 1 … 5
      Record Meshed Geometry      Read On First Or Write · Cache/2-outside-roughing-done.wct
▸ 3 · Outside finishing           Machining Resolution 0.2 mm
      finishing 1 … 7
      Record Meshed Geometry      Read On First Or Write · Cache/3-outside-finishing-done.wct
▸ 4 · Pocket roughing             Machining Resolution 0.5 mm
  …
▸ 8 · Deburring                   Machining Resolution 0.2 mm
Post-Execution                    geometry diff on

Why this shape:

  • The records are part of the shape, not an optimisation. Where the stock is an STL, keep a Record Meshed Geometry entry above the first group: the stock is meshed before the first line of NC plays, at the workpiece's Initial Resolution, and for a finely tessellated STL that costs minutes on every play until it is cached. Close each group with an end-of-stage record named after the group, so a later run can resume from any of them. The cached stock does not follow a later change to the stock geometry or its Initial Resolution until its file is deleted. A Mission That Resumes is the layout in full.
  • Grouping must not reorder. The stock evolves from one operation to the next, so the sequence is load-bearing. Group adjacent operations; never sort the list into tidy families if that moves an operation past another one.
  • Each group carries its own machining resolution. Roughing is about force, not surface, so a coarse resolution is enough and runs quickly. Finishing, rib and deburring passes leave the final surface with small and ball cutters and deserve a finer one. Setting the resolution mid-mission only changes the value the act runner cuts with — the workpiece is not rebuilt, and the geometry container is adaptive — so alternating coarse and fine between groups is safe.
  • A group is a switch. Disabling one List skips a whole phase, which is how you re-run just the finishing passes without deleting anything.
  • Iterate on a short program; keep the full job beside it, switched off. A replay of the whole job is the acceptance run, not the debugging loop. While datums, tools and offsets are still moving, play a derived main program that runs one representative piece of the job — for four identical parts held on one fixture plate, the block that machines the first part — in a List of its own, and keep the full program in a second List with its checkbox cleared; PUT /api/Mission/commands/{path}/enabled with a body of true or false flips which one runs. Put the stage-0 record at root level above both groups so either variant starts from the same cached stock, and give each variant its own end-of-stage file. The first plays of a new project are the ones most likely to produce no steps or to cut in the wrong place, and a one-part program shows that in a fraction of the time.
  • Budget the fine groups. Machining resolution drives the cost of subtracting material, not the number of simulation steps — the step count follows the motion resolution and does not change. On one 33-operation job, refining the finishing groups from 0.5 mm to 0.2 mm left the step count identical and multiplied wall-clock time by about 2.8. Decide the finish resolution with that trade-off in view, and say what it costs in the project's README so nobody is surprised.
  • Setup commands are granular. Machining resolution, motion resolution, collision detection, pause-on-failure and physics are separate commands, so each can sit exactly where it should take effect — global ones at the top, per-phase ones inside a group.

Over HTTP the mission is built one entry at a time. POST /api/Mission/list-command/entries adds at the root and POST /api/Mission/list-command/entries/{path} inside a list, with a body of {"CommandType": "<kind>", "IsEnabled": true} and an optional "InsertIndex"; the reply's path is the new entry's address for every later call. A path is the chain of indices joined by dots — 6 is the seventh root entry, 6.1 the second entry inside it. The values the routes expect:

What Values
CommandType the command's class name without Command, lower-cased: list, ncfile, manualtooling, nccode, script, machiningresolution, machiningmotionresolution, collisiondetection, pauseonfailure, physics, recordmeshedgeom, postexecution, exportmeshedgeom, ncoptoption; GET /api/Mission/command-catalog lists the ones this service can add
List title PUT /api/Mission/commands/{path}/listcommand with {"Title": "…"}
Program file PUT /api/Mission/commands/{path}/ncfile with the project-relative path as a JSON string
Manual Tooling PUT /api/Mission/commands/{path}/fields/toolId with the tool number
On/off Setup commands PUT /api/Mission/commands/{path}/fields/enable with true or false
Machining resolution PUT /api/Mission/commands/{path}/fields/machiningResolution_mm with a number
Motion resolution PUT /api/Mission/commands/{path}/machiningmotionresolution/type with "feedpercycle", "feedpertooth" or "fixed"
Record Meshed Geometry PUT /api/Mission/commands/{path}/recordmeshedgeom/main-action with "ReadOnFirstOrWrite", and …/rel-file with the cache file
Post-Execution one PUT /api/Mission/commands/{path}/postexecution/… route per value: enable-write-step-files and enable-write-shot-files with true or false, step-file-template and shot-file-template with the template as a JSON string, relative to the project folder, shot-file-time-resolution-ms with a number, enable-geom-diff with true or false; GET /api/Mission/commands/{path} reads them back

GET /api/Mission/commands/{path}/fields lists a command's field keys when the one you need is not in the table. Post-Execution has none: its fields list is empty, and its values go through the postexecution/… routes above.

Tip

A project that loads an ideal (target) geometry and never compares against it is unfinished. Turn on the geometry diff in the Post-Execution command so a full replay ends by measuring the machined stock against the finished part. A diff with no target to compare against does nothing and still reports that it was built — Geometry Validation says how to tell.

4. Fill the controller's own tables

The tool house describes the physical tool. The controller has its own record of the same tool, and the NC program reads the controller's copy. Both must exist.

For a Siemens-style controller that is two tables:

Table Holds If empty
Tool name → tool number the names a T="…" call uses the tool never mounts, and the program produces no machining steps at all
Tool edge offsets per (tool, edge): length along the tool axis, radius every edge call falls back to the generic offset table and logs a warning

Take the values from the tool house and assert they match before writing. When the warning disappears and the cut does not move at all, that is the proof the rows carry the right numbers — if the geometry shifts, one of the two tables was wrong.

Note

Not every controller parameter has a consumer. Some, such as the maximum spindle speed, are recorded machine data that nothing in the pipeline reads today. Filling them is good hygiene, but do not report it as fixing a defect, and remember that copying a guessed value (an assumed spindle, say) into a second place means both must move when the real datum arrives.

Work offsets over the API

The service exposes two work-offset tables, and they are not kept in step:

Table Routes Read by
The runner's own table — on Fanuc and Mazak the parameter table (#5221+, #7001+) GET /api/mech/soft-nc-runner/work-coordinates, PUT …/work-coordinates/{id} {X, Y, Z} the run, and the Equipment page's Work Coordinates leaf
The legacy ISO coordinate table GET /api/Controller/iso-coordinate-table, PUT …/iso-coordinate-table/{index} {Index, X, Y, Z} the align call below, the Execution page's coordinate marker, and scripts that name it

Write every row the program selects into the runner's table, then read it back after a save and a reload. A build that fills only the legacy table plays the whole program around machine zero: nothing is cut, and the only symptom is one Play-Touch--None warning, because a G54–G59 row left at zero is read as an authoring convention and stays silent. When the align call is used, write the same row into the legacy table as well. Its PUT edits only rows the table already holds (G54–G59 and G59.1–G59.9) and answers 400 for any other id.

A row is the machine position at which the spindle's tool mount meets program zero, with every axis at machine zero. Tool length is not part of it: the height offset selected by G43 H adds the holder length plus the stick-out. An offset recorded with a tool touched off and no length compensation in the program already contains that tool's length, and has to be converted before it is entered. The program still adds no length, so the run needs it from elsewhere, or the tip runs a whole tool length below every programmed Z: a G43 H in the program's copy (§1), or, once the part is placed, the length put back into the Z of the runner's row, one row per tool, as the shop keeps it.

Placing the part on a recorded offset over the API

The reproducing direction — keep the recorded offset, move the part onto it — is one call, POST /api/Controller/iso-coordinate-table/{index}/align-workpiece-program-zero. It reads row {index} from the legacy table and rewrites the transformer of the fixture branch from its Geometry Anchor to its Table Mount as the static translation that puts the Program-Zero Anchor on that offset with every axis at machine zero. The answer carries the translation and the transformer before and after, as XML; posting the “before” XML to …/iso-coordinate-table/revert-align-workpiece-program-zero undoes it. A mission script has the same operation as AlignWorkpieceProgramZeroToIso("G54").

  • It needs a fixture on the project and a machine chain with linear (and optional rotary) axes. Without either it answers 400 and changes nothing; a pure cutter-location device is not such a chain.
  • The branch it writes is the one leading from the fixture's Geometry Anchor straight to its Table Mount — every fixture in the default shape has it. A fixture without that branch, one whose branch carries a dynamic transformer, or one where the way from the table to program zero does not pass through that branch alone answers 400 with the reason, and changes nothing.
  • It replaces whatever that branch held, rotation included, with a pure translation.
  • It reads the workpiece geometry frame and the workpiece branches to its Fixture Mount and its Program-Zero Anchor as they stand, once. Set those first, and align again after changing any of them.

Prove the placement with a probe rather than trusting the call. Before the first play or after a Reset — until then a played session keeps the placement it started with — press P0 on a row the program never uses: POST /api/Controller/iso-coordinate-table/G59/set-to-program-zero answers with the X, Y and Z it wrote, which must equal the recorded row to rounding. Return the probe row to machine zero with …/G59/set-to-machine-zero before saving. The runner's POST /api/mech/soft-nc-runner/work-coordinates/{id}/set-to-program-zero answers only success; read its value with GET /api/mech/soft-nc-runner/work-coordinates.

Once aligned, an error in the recorded offset no longer changes what is cut, but it moves the whole setup in machine coordinates, where stroke checks and machine collisions see it. For several parts on one table, one offset each, P0 fills only the row of the part carrying the program-zero anchor — see Several parts, several offsets.

5. The finish line is a clean replay, not a filled form

Replay the whole mission and inventory the messages by id. Every warning is an item on the to-do list. Resist writing one up as an acceptable fallback: a fallback firing means the project never supplied the data the fallback exists to cover.

Read messages at a low severity so nothing hides. The severities are Message, Success, Progress, Warning, Error — note there is no Info, and an unrecognised name is rejected in the response body rather than by an HTTP error, which reads exactly like “no messages”.

Stop when only declared informational messages remain — the ones that state a known, deliberate limitation — plus the expected messages of constructs left as delivered (§1), each listed in the README with its id and the reason it does not change the result. Typical survivors:

  • one per NC file, reporting the line count;
  • machine M-codes that the runner recognises and consumes but does not model (OEM auxiliary functions such as coolant valves), which are announced rather than silently ignored.

Anything else is work.

A clean message list is not yet a pass. A run started without a Reset plays on top of the one before it, whose touching steps keep Play-Touch--None from firing; a re-run on stock already cut leaves a Tool-Touch--None for each tool that cuts nothing instead, which reads as a tool-length or offset fault rather than as the skipped Reset. Two other failures leave one warning each: a run that never mounted a tool produces no steps at all and still ends Finished, with Step-Tool--NotEquipped in the step list rather than an end-of-play warning (a Manual Tooling entry above the program mounts the tool); a Post-Execution with no program above it writes nothing and reports StepFile-Write--NoFile / ShotFile-Write--NoFile. All three are covered, with the checks that catch them, in Replay Acceptance over the HTTP API.

6. Two numeric cross-checks — and what they do not prove

Messages catch what the runner noticed. These two catch datum errors that produce no message at all.

Depth of cut against the programmed layer depth. On a roughing pass whose step-down you can read from the NC, the peak cutting depth must equal it. This pins the Z datum: if program zero is wrong, the tool cuts a different amount and the peak does not land on the programmed number. A finishing program has no layer depth to read; check its datum against the surface it leaves instead (Building the Workpiece from a Reference Mesh).

Cycle time against the post-processor's own estimate. Many post-processors write a per-operation machining time into the program as a comment. Sum them and compare with the simulated time.

Warning

Scope the cycle-time check honestly. It confirms that feed rates and path length are consumed correctly. It is not a comparison against the real machine, because both sides ignore the same things: there is no acceleration model (time is path length over feed rate, with no ramps — which costs the most on finishing passes made of thousands of short segments), tool-change time defaults to zero, and the home and tool-change positions default to machine zero (on a tool change Z goes to 0 while X and Y stay) until the controller's Home / G28 Reference and Tool-Change Position tables are filled from machine data.

For the same reason, a customer's “total machining time” running well above the sum of the per-operation comments is usually not a contradiction — the comments are cutting time, the total is wall clock including tool changes and inter-operation moves. Two different quantities.

7. Record every assumption

Real deliverables are incomplete. Build with sensible values, and write each one down in a README.md beside the .hincproj — what was assumed, what it was inferred from, and what changes when the real value arrives. Recurring items: work offsets, blank size and material, tool helix and rake angle, coating, stick-out, holder dimensions, the smart holder's sensor height, the spindle's power–torque curve, fixture geometry, the rewrites made to program copies, and the cycle-time inputs above.

Cross-reference: Project Data Checklist is the list to hand the customer before building; this section is what you hand back after.