Table of Contents

Driving the Web Service over HTTP

The HiNC web service exposes everything its browser client does as a REST API, so a script or an agent can build, save and run a project with no browser open. This page is what such a caller needs around its requests: where the contract is and where it is wrong, how to sign in and read a reply, how files and projects are addressed, the key-based pattern most setup routes share, and how to replay a mission and color its steps by condition without disturbing anyone else. The order in which a project is assembled is Project Construction.

Contract and Sign-In

The service publishes its contract as two OpenAPI documents, both served in every environment:

  • The Swagger document, opened from the Swagger UI at /swagger and served at /swagger/<contract version>/swagger.json — currently /swagger/v3.1/swagger.json. The UI always loads the current one, so a guessed /swagger/v1/swagger.json is a 404, not a missing contract. The UI and its document answer ahead of the sign-in stage and are readable without a session.
  • The framework document at /openapi/v1.json. It is an ordinary routed endpoint, so while the login gate is on it needs the session cookie like any other route.

Routes and query parameters agree between the two. Request bodies do not. The framework document names every schema by its simple type name, so request types that share a name in different controllers collapse into one schema, and several routes advertise another route's body. Take body shapes from the Swagger document, which names schemas by full type name. Where only the framework document is at hand, these are the routes it gets wrong:

Route Shown as Actually reads
POST /api/mech/machine-tool/load { xmlText, loadResources } { rootName, relFile }, with rootName either ProjectDirectory or ResourceDir
POST /api/Workpiece/workpiece-material/load { xmlText, loadResources } { rootName, relFile }
POST /api/Workpiece/cutting-parameter/load { xmlText, loadResources } { rootName, relFile }
POST /api/mech/spindle-capability/load { xmlText, loadResources } { xmlText, baseDirectory, relFile }; loading by path is POST /api/mech/spindle-capability/load-file with { rootName, relFile }
POST /api/file-explorer/rename { name } { rootName, relativePath, newRelativePath }

JSON property names are matched case-insensitively, so RootName and rootName are the same field, and enumerations travel as their names. Values are not relaxed the same way: root names and geometry or transformer kind names are compared exactly as written on this page.

Check the login gate before anything else. The shipped configuration leaves it off, so an installed copy answers every route with no session, on its own computer only: its address is localhost, and while the gate is off a request addressed to any host name but localhost, 127.0.0.1 or [::1] is answered 400 Bad Request unless the service's AllowedHosts lists that name, so widening the address alone does not let a script on another computer in. A deployment can turn the gate on, which lifts that host-name restriction unless it sets AllowedHosts, and while the gate is on every route outside /api/auth answers 401 with an empty body until the caller has signed in — which reads exactly like a service that is not running. GET /api/auth/status is always anonymous and reports whether the gate is enabled, whether this caller is signed in, and the service version. Sign in with POST /api/auth/login and the JSON body { username, password }. A good pair answers { authenticated: true, … } and sets a cookie every later request must carry, so use an HTTP client with a cookie jar; a wrong pair answers 401 with the code InvalidCredentials. With the gate off, the login answers success without reading the body, so a script may sign in unconditionally. A script in a browser page works only from a page the service served itself: the service sends no CORS headers, so no other page may read its answers, and while the gate is off it answers 403 to another page's requests — one on another port of the same computer included. A script outside a browser sends neither Origin nor Sec-Fetch-Site and is not affected. Some routes answer with data but are POSTs, because they also register the object they return for later editing, write files as they serialize, or open a path you name: the Tool House holder and beam reads (CylindroidHolder/Get, FreeformHolder/Get, Cutter/{id}/inner-beam, Cutter/{id}/upper-beam), ObjectManagement/GetXml, mech/machine-tool/xml, NcProgram/file-lines and StlFile/GetFileInfo. A GET to one of them answers 404, as a route that does not exist does (Reading a Reply). A few GETs still fill in a controller parameter table's missing default as they read it, which a later project save writes out.

Then read GET /api/Project/status once. It reports whether a project is open, its path, and the absolute admin and project folders on the server. Confirm the admin folder is the tree meant to be written into before writing anything: a service started from a different configuration comes up just as healthy, pointed at a different tree.

Reading a Reply

Routes take their input in one of three shapes, and the Swagger document says which: a JSON object body such as { rootName, relFile }; query parameters, which is how most key-based edit routes take both their key and their values — a route that takes a list, such as POST /api/Cylindroid/UpdateAllPairs, takes the key as a query parameter and the list as a JSON body (see Editing Objects by Key); or a bare JSON value as the whole body — PUT /api/Workpiece/init-resolution takes the number itself, 0.25, not an object.

An HTTP 200 does not by itself mean a request did what it asked. The service keeps the convention its browser client relies on: a refusal that is not a malformed request comes back as 200 with success: false and a message, and some partial successes come back as success: true with a warning. For example:

  • POST /api/mech/machine-tool/load naming a file that does not exist, or called with no project open, answers 200 with success: false; so do the workpiece-material and cutting-parameter loaders for a missing file or one that does not deserialize;
  • POST /api/StlFile/UpdateSource whose file cannot be read answers 200 with success: true and a warning — the reference is stored anyway; a new StlFile then has no mesh, while one that already held a mesh keeps the old one, which GetInfo goes on reporting and the next save writes under the new name, so compare triangleCount with the file, not only loaded;
  • POST /api/Mission/list-command/entries with a command kind the catalog does not know answers 200 with success: false;
  • POST /api/Workpiece/workpiece-material/load and the cutter's flute-material/load, shank-material/load and coating-layers/{index}/material/load install a material whose thermal constants are implausible as written, or not given — an element the file lacks takes no default, and the thermal model is skipped for the tool (for every tool when it is the workpiece material) — and carry the warning in messages (Material-ThermalConstants--NotGiven, Material-ThermalConstants--NotFinitePositive, Material-ThermalConstants--OutOfRange; the flute and coating-layer loads also Material-WearCurves--NotGiven, a cutter material whose file lacks its TemperatureVsHardnessCurve or TemperatureVsWearCoefficientCurve: installed, named, the native thermal model does not run for the tool), not in warning.

Malformed requests answer 400 — an unknown root name, a path that leaves its root, an empty required field. An object-store key that does not exist answers 404, and a key holding the wrong kind of object answers 409. Most routes that create an object in the store answer with its key as a bare string — plain text, unless the request's Accept header asks for JSON, which yields a quoted JSON string; a few, such as POST /api/Cutter/{id}/upper-beam/create, answer a JSON object carrying the key.

A route that does not exist answers 404. A GET or HEAD to a path under /api that no route serves — a misspelt route, or one that takes only POST — answers 404 with an application/problem+json body whose detail names the method and the path; any other method answers 405. No route answers HEAD, so a HEAD answers 404 even where a GET would answer. A path outside /api belongs to the application's page, so a GET that leaves out the /api prefix answers 200 with HTML (text/html). Check that the reply's Content-Type is the one the route answers with — application/json for a JSON reply, text/plain for a bare key, a file type for a download — before reading anything from it.

A reliable caller therefore checks the status code, then success, then warning and messages, and follows each write with the matching read before building on it: GET /api/mech/machine-tool after a chain load, GET /api/StlFile/GetInfo (loaded, triangleCount) after pointing an STL at a file, GET /api/Workpiece/Status once the workpiece is set up.

One Service, One Project

A service instance holds exactly one open project and one object store, shared by every browser and every script connected to it. Signing in decides who may connect, not whose project it is. POST /api/Project/new or POST /api/Project/load replaces the project everyone on that instance is working on, and the run controls act on whatever run is current: POST /api/Execution/stop takes no run identifier and stops the run in progress, whoever started it.

Before driving an instance other people may be using:

  • Read GET /api/Project/status and GET /api/Execution/status. Do not take over while another person's project is open or a run is Running or Paused; a dedicated instance is the safe choice for a scripted build.
  • Give every object-store key a prefix no other caller uses (see Editing Objects by Key).
  • Identify a run by the runId that POST /api/Execution/start returns and the instanceId beside it — the service process the runId counts in — and never stop a run from a timeout (see Running a Replay).
  • Do not edit the tool house, or the machine tool's and the fixture's solids, while a run is live. While the player is locked (Running or Paused) every non-GET route of /api/ToolHouse, /api/Cutter, /api/CylindroidHolder and /api/FreeformHolder answers 409 { success: false, code: "ToolEditRefusedWhileRunning" } and changes nothing, except the index-only POSTs the tool-house page issues on every open (Initialize, SelectTool, {id}/inner-beam, {id}/upper-beam, the holders' Get and IndexCurrentGeometry); stop or reset first. While it is locked, and while a play a host request drives directly on the session shell is in flight (POST /api/SessionShell/PlayNcFile and its siblings, which executionStatus does not show), the generic object-store routes (/api/Box3d, /api/Cylindroid, /api/StlFile, /api/TransformationGeom, /api/StaticTranslation, …) answer 409 { success: false, code: "SolidEditRefusedWhileRunning" } to a write whose key names an object under the machine tool's or the fixture's solids, which the runtime copy shares with the setup instead of copying them (their index-only POSTs, such as TransformationGeom/IndexGeom, pass; TransformationGeom/IndexTransformer on a wrapper there that has no transformer yet is refused, since it would write a NoTransform on the null field), and the six solid routes of /api/mech/equipment-topology — enable-geom, disable-geom, create-geom, set-geom-transform, refresh-geom-cache and POST …/rgb — answer the same, as does its index-geom on an anchor that has no solid yet, since it would hang one. For a tool the generic routes are not gated by the player: a script must not send them against a tool's keys during a run either — the cutter they edit is the one the run cuts with, and only the resync that follows would be refused.
  • Do not teleport, optimize or convert over HTTP while a run is live. While the player is locked (Running or Paused), POST /api/SessionShell/PlayToolingTeleport and PlayClTeleport play nothing, and OptimizeToFiles and ConvertClToNcFiles write nothing: each answers 409 with success: false and one entry in messages whose id names the reason — Play--BusyAct, OptimizeToFiles--Busy, ConvertClToNc--Busy — and the same message is in the shell list of GET /api/Execution/messages. A teleport is refused the same way with Play--BusyActAnotherThread while another request plays an NC program on the session, and with Play--Reviewing while a play result is under review. The program plays (PlayNcFile, PlayNc, …) answer 409 the same way: Play--Busy while the player is locked, Play--BusyAnotherThread while another request plays, Play--BusyReset while the player is being reset. Make these calls after the run.
  • Do not call the session or the player while the project is being changed or the runtime reset. While a project New, Load, Reload or Close runs, or another request's POST /api/SessionShell/ResetRuntime or EndSession is resetting the runtime or ending the session, the plays answer 409 Play--BusyReset and the teleports Play--BusyActReset, BeginSession, ResetRuntime, EndSession and Reset answer BeginSession--BusyReset, ResetRuntime--BusyReset, EndSession--BusyReset and Reset--BusyReset, and POST /api/Execution/start, run-line and run-step answer 409 { success: false, code: "Start--BusyReset", message }, as POST /api/Execution/reset answers Reset--BusyReset. While another request plays a teleport, ResetRuntime, EndSession and the two resets answer ResetRuntime--BusyAct, EndSession--BusyAct and Reset--BusyAct, and a project New, Load, Reload or Close answers 409 with a message naming the play, the act or the reset it waits for. Wait for it to end, then call again.

Files and Named Roots

Every file path the API takes is relative to one of three named roots, passed by name:

Root name Resolves to
AdminDirectory the service's working area; the project paths given to /api/Project/* are relative to it
ProjectDirectory the folder holding the open project file — unavailable, and refused with 400, while no project is open
ResourceDir the shipped library, the Resource folder under the admin area

GET /api/file-explorer/roots lists the roots available at the moment. A path that leaves its root is refused with 400. The paths a mission command stores take no root name: a Program File's program and a Post-Execution template are relative to the project folder, the one ProjectDirectory names, and an absolute template is used as it is.

Files reach the server through the routes under /api/file-explorer:

Route Arguments
POST upload query rootName and relativePath; the file as the multipart form field file. Missing parent folders are created and an existing file is overwritten.
POST write-text body { rootName, relativePath, content }; written as UTF-8 without a byte-order mark, parent folders created
POST create-directory body { rootName, relativePath }; a no-op when the folder exists
GET list, GET download, DELETE delete query rootName and relativePath; delete removes a folder recursively

There is no cross-root copy: copy duplicates beside the source and rename refuses to move between roots. Because ProjectDirectory exists only once a project is open, inputs staged before the project is created go to AdminDirectory, under the folder the project file will sit in — <project folder>/NC/…, <project folder>/Geom/… — and are addressed through ProjectDirectory afterwards.

What a loader does with a file depends on where it came from:

  • Pre-prepared resources — machine-tool chain, spindle capability (load-file), workpiece material, cutting parameters, cutter material — are loaded by { rootName, relFile }. A file already inside the project is referenced where it is. A file picked under ResourceDir is written into the project at the same sub-path, for example WorkpieceMaterial/<name>, and referenced there, so the saved project never points into the shared library; a file picked elsewhere under AdminDirectory is written into the project root under its own name. Uploading the file into the project first and loading it from ProjectDirectory comes to the same thing, and it is the form to use whenever the copy is not there: after a pick from the library, list the project's folder (GET /api/file-explorer/list?rootName=ProjectDirectory&relativePath=WorkpieceMaterial, and so on) and check the file landed — an instance whose loaders do not copy stores a reference that resolves to nothing, and the next save or close fails with Could not find file. The machine-tool loader accepts only ProjectDirectory and ResourceDir, and a chain picked from the library comes with its side files.
  • An STL set with POST /api/StlFile/UpdateSource and { stlFileKey, rootName, relFile } is recorded project-relative; one picked outside the project is recorded as Geom/<file name>, and its mesh is written there at the latest on the next save — but only when no file already sits at that path: an existing Geom/<file name> is kept and the reference then names it. Give meshes from different folders distinct names, or upload them into the project first.
  • NC programs are recorded as the plain path a mission command names. Nothing copies them: upload them.

Project Lifecycle

The project routes work on the one open project, addressed by a path relative to AdminDirectory:

Route Body Effect
GET /api/Project/status — hasProject, projectPath, and the absolute admin and project folders
POST /api/Project/new { relativePath } creates the folder if needed, writes the new project file at once and opens it; refused with 400 when a file already exists at that path
POST /api/Project/load { relativePath } opens a project; the reply's messages lists load-time holes, such as a missing STL, a referenced side file that cannot be read (XmlSource-Read--Failed) or an older project's unreadable script file (ScriptFile-Read--Failed), none of which fails the load; a project file that itself cannot be read — missing, locked, malformed — is refused with 400, the message naming the file under its root, and no project is open afterwards (the open project is released before the file is read; the status hub reports it)
POST /api/Project/reload — re-reads the open project from its file, with the same messages; a file deleted, locked or damaged since is refused with 400 and no project is open afterwards: the in-memory project is released before the file is read
POST /api/Project/save, close — act on the open project
POST /api/Project/saveas { relativePath } writes to a new path, which becomes the open project; a file already there is overwritten without a prompt

save, reload, close and saveas answer 400 when no project is open. The six project-file operations take turns: one requested while another is running is refused with 409 rather than queued.

A new project is not empty. It carries a fixture in its default shape with an empty solid on its geometry anchor, a workpiece with no geometry, an empty tool house and no machine chain, and a mission seeded with eight root entries: Machining Resolution, Machining Motion Resolution, Collision Detection, Pause on Failure, Physics, two Program File entries naming the placeholders NC/NC-File-Name-1.nc and NC/NC-File-Name-2.nc, and a Post-Execution. The placeholder programs do not exist, and each one left enabled raises the error RunNcFile--NotFound on every play and is skipped; the play goes on. An entry added through POST /api/Mission/list-command/entries without an insertIndex lands at the end, after the seeded Post-Execution. A Post-Execution writes only what has played above it, so nothing a program added there plays reaches its outputs, and when nothing above it played either, its writers end on StepFile-Write--NoFile and ShotFile-Write--NoFile (for the outputs switched on) instead of finishing. Add a program with "insertIndex" set to the Post-Execution's current index — 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 in place, or delete them with DELETE /api/Mission/list-command/entries/{path}, highest index first, because the entries after a deleted one renumber; then read the tree back with GET /api/Mission/list-command/entries.

Rebuilding at the same path is close, delete, new: POST /api/Project/close, then DELETE /api/file-explorer/delete?rootName=AdminDirectory&relativePath=<project path>, then POST /api/Project/new. The other files in the folder — uploaded programs and resources — are left as they are.

Finish every build with save, reload and a read-back. Reload re-reads the file, so the settings read after it are the ones the file actually holds; a value that does not survive the round trip was never saved.

Editing Objects by Key

Geometry, transformers, cylindroid profiles and similar setup values are edited through an object store rather than through one route per field. A caller works in three moves:

  1. Create or index an object and get its key. A create route builds an object, installs it where it belongs, stores it under a key and returns that key. The key is passed as a query argument (geometryKey on the workpiece's create routes, key on the per-kind New and NewWithValue, …); when it is left out, a generated one is used and returned. An index route publishes an object that already exists — the geometry a workpiece already holds, the transformer inside a transformation geometry — and returns its key.
  2. Edit through the key. Per-kind routes take the key as a query parameter and the new values as query parameters too — POST /api/StaticTranslation/UpdateTrans?staticTranslationKey=…&x=…&y=…&z=… — or a list as a JSON body.
  3. Read back through the key — GET /api/StaticTranslation/Get?staticTranslationKey=…, GET /api/StlFile/GetInfo?stlFileKey=…, or GET /api/Index/GetType?key=… for any key.

A vector is never an object of its own. The translation of a static translation, the axis and pivot of a rotation and the two corners of a box are values of the object that owns them, and are written through that owner's route with the owner's key and the three axes as query parameters: StaticTranslation/UpdateTrans, StaticRotation/UpdateAxis and UpdatePivot, DynamicRotation/UpdateAxis and UpdatePivot, DynamicTranslation/UpdateAxis, and Box3d/Update with both corners. The owner's Get reads the value back, and the two rotations and the dynamic translation also answer NormalizeAxis. No route hands out a key to a vector.

Container routes derive the keys of what they create from the container's own key, so a chain of calls can be scripted without parsing replies:

Call Key returned
POST /api/Workpiece/CreateRawGeometry?geometryKey=K&geometryType=TransformationGeom K
POST /api/TransformationGeom/CreateGeom?transformationGeomKey=K&geometryType=StlFile K-Geom
POST /api/TransformationGeom/CreateTransformer?transformationGeomKey=K&transformerType=StaticTranslation K-Transformer
POST /api/Workpiece/SetRawGeometryTransform?geometryKey=K&transformerType=StaticTranslation K — the geometry already in the slot, wrapped in place; K-Geom and K-Transformer are kept in step, and transformerType=None unwraps it again
POST /api/Workpiece/Initialize?sessionKey=S a JSON object naming S-Workpiece, S-RawGeom and S-IdealGeom; a geometry key is indexed only when its slot holds something (hasRawGeom, hasIdealGeom)
POST /api/Fixture/Initialize?sessionKey=S a JSON object naming S-Fixture
POST /api/mech/equipment-topology/{owner}/branch/{guid}/index-transformer equipment-topology.<owner>.branch.<guid>.transformer — the transformer on that branch of the fixture, workpiece or machine tool; a branch carrying none is first given a pass-through one

Placing a mesh as the stock, translated into position, therefore runs as below, here with the caller's own prefix job7-stock and a mesh already uploaded to Geom/stock.stl:

POST /api/Workpiece/CreateRawGeometry?geometryKey=job7-stock&geometryType=TransformationGeom
POST /api/TransformationGeom/CreateGeom?transformationGeomKey=job7-stock&geometryType=StlFile
POST /api/StlFile/UpdateSource        { "stlFileKey": "job7-stock-Geom", "rootName": "ProjectDirectory", "relFile": "Geom/stock.stl" }
GET  /api/StlFile/GetInfo?stlFileKey=job7-stock-Geom                 -> loaded: true, triangleCount > 0
POST /api/TransformationGeom/CreateTransformer?transformationGeomKey=job7-stock&transformerType=StaticTranslation
POST /api/StaticTranslation/UpdateTrans?staticTranslationKey=job7-stock-Transformer&x=-45&y=-30&z=-12
POST /api/Workpiece/UpdateRawGeometry?geometryKey=job7-stock        (re-install, next section)
POST /api/Workpiece/ClearRawGeomCache

A create route replaces whatever the slot held; nothing carries over from the object it displaced. Wrapping what is already there is a separate family, the owners' Set…Transform routes — Workpiece/SetRawGeometryTransform, Workpiece/SetIdealGeometryTransform, mech/equipment-topology/{owner}/anchor/{id}/set-geom-transform, FreeformHolder/SetGeometryTransform, GeomCombination/SetItemTransformAt, TransformationGeom/SetGeomTransform and Cutter/{id}/upper-beam/set-transform — each taking transformerType, a transformer kind or None: a bare geometry is wrapped in a TransformationGeom around that very object, a wrapped one gets a fresh transformer of that kind, and None hands the inner geometry back to the slot; the route answers the slot's key. These are what the web client's Transform picker posts, and the client never names TransformationGeom as a kind; the sequence above stays valid for a script and builds the same wrapper.

Anchors and branches of the equipment

The transformers that place the fixture on the table and the workpiece on the fixture are not slots with routes of their own. Each is the transformer of a branch in the topology of the part that owns it, and the routes under /api/mech/equipment-topology address a part's anchors and branches by guid, with the owner — machine-tool, fixture or workpiece — in the path:

  1. GET /api/mech/equipment-topology/graph lists every anchor (guid, owner, roles) and every branch (guid, owner, fletchAnchorGuid, arrowAnchorGuid, transformerType, isAttachment, isErrorTerm). Find a branch by the role of the anchor it arrives at, never by its name: the workpiece branch whose arrowAnchorGuid is the workpiece anchor whose roles include ProgramZeroAnchor is the one that places program zero; the one arriving at FixtureMount places the workpiece on the fixture; the fixture branch arriving at TableMount places the fixture on the table. Skip the attachments (isAttachment: true, owner equipment) — the equipment builds those itself from the roles.
  2. Build a transformer under a key of the caller's own — POST /api/StaticTranslation/NewWithValue?key=K&x=…&y=…&z=… — and install it with POST /api/mech/equipment-topology/{owner}/branch/{guid}/update-transformer?transformerKey=K. The answer is the canonical key the transformer is re-indexed under, equipment-topology.<owner>.branch.<guid>.transformer, which index-transformer on the same branch also answers; editing through that key is the nested-edit case of the next section.
  3. A role is bound with POST /api/mech/equipment-topology/roles/{owner}/{role} and the body { "guid": "…" } — an anchor guid for TableMount, ToolMount, WorkpieceMount, GeomAnchor, FixtureMount and ProgramZeroAnchor, a branch guid for AxisX … AxisC and Spindle; an empty guid unbinds. GET roles/{owner} lists each role with its candidates and the checks that fail (Unbound, NotOwn, Duplicate, NotReachable, AxisTransformerMismatch, AxisOffChain, SpindleOffToolAxis, and for marks where none belongs ErrorTermRole, ErrorTermDynamic, ErrorTermOnToolAxis, ErrorTermNotOnRole), and gives each axis and the spindle the guid of its error term, errorTermBranchGuid, empty when it has none.
  4. An error term belongs to an axis or the spindle of the machine tool. It is added with POST /api/mech/equipment-topology/roles/machine-tool/{role}/error-term, {role} being AxisX … AxisC or Spindle, with no body: in one edit a new anchor and the marked error term — a Static Rotation of 0° about (0, 0, 1) until a value is written — go in just before the role's branch, and that branch is rebuilt from the new anchor under a new guid. The answer carries the role, the new anchor, the errorTerm and the role's branch. DELETE on the same route takes the error term and its anchor out and answers the role's branch, under a new guid again; take the guid from the answer, or read the roles again, before addressing that branch. The error's value is the error term's transformer, set with index-transformer and update-transformer like any branch's, static kinds only. The mark by itself is set and cleared with POST /api/mech/equipment-topology/{owner}/branch/{guid}/error-term: the body { "value": false } clears it, and { "value": true } sets it again only on a branch that stands where an error term stands, so a script can compare a play with the marks cleared, in which the controller knows the errors, against one with them set. Posting the value the branch already holds changes nothing. The project's error-term switch is GET / PUT /api/mech/equipment-topology/error-terms, a bare JSON true or false as the whole body. Where an error term stands is in Error-Term Branches; a request the editor does not accept answers 409 with a code naming the reason, and the HTTP Surface table lists those codes with every other route of this controller.

Every topology edit under this controller, an error term added or removed and a mark set or cleared included, rewires the equipment, and the attachment branches are given new guids each time, so read the graph again before addressing one; writing the error-term switch rewires nothing. The worktable attachment — from the machine's table end to the fixture's TableMount, or to the workpiece's FixtureMount when there is no fixture — is the one attachment with an editable transformer, addressed with the owner equipment (…/equipment/branch/{guid}/index-transformer and update-transformer; its key is equipment-topology.equipment.table-to-comp); any other attachment answers 409 AttachmentFixed. The refusals carry a code: 400 UnknownOwner, 404 MechanismNotFound when the part is not installed, 409 MechanismNotEditable for a cutter-location device, 409 RoleElementTaken when the element already plays another role of the same part, 409 AxisTransformerMismatch when an axis is given, or a bound axis branch is set to, a transformer of the wrong kind, 409 TransformerInUse when the object under transformerKey already sits on another branch (build one transformer per branch; re-posting the one a branch already carries is never refused), and 409 SolidsNotAllowed for a geometry route on the workpiece — its geometry is the raw and target slots above, not an anchor solid.

The store is shared by every caller of the service and its keys are plain strings. Writing a key that already exists silently repoints it, so every caller needs a key prefix no other caller uses. Keys stay until removed with POST /api/Index/Remove?key=…; removing a key only forgets the handle and never takes the object out of the project.

Making an Edit Reach the Run

A project holds its equipment twice: the setup that is saved, and a runtime copy that the runner, the physics, collision checking and the stock mesh are built from. The runtime copy is not patched field by field — apart from the environment values (spindle capability, coolant condition, ambient temperature), which the spindle-capability routes forward at once. It is rebuilt from the setup at two moments:

  • at the start of a fresh session — the first play after a project is loaded, or after a Reset;
  • when an install route marks the setup edited — at once when no session is open, otherwise at the next play or Reset. The install routes are the ones that put an object into a slot or edit the equipment's topology: creating or replacing a workpiece geometry, installing a transformer on a branch with update-transformer, binding a role, adding or deleting an anchor or a branch, adding or removing an error term, setting or clearing a mark, writing the project's error-term switch, loading a machine chain, a material or cutting parameters, aligning the workpiece to a work offset.

The status says when an install is waiting. GET /api/Execution/status answers hasSession — something has played since the last Reset — and setupEquipmentDirty — an install route has marked the setup edited and the runtime copy has not been rebuilt from it yet — beside resetCount, which each Reset raises by one, and instanceId, the identity of the service process the runId counts in (instanceStartedUtc says when that process started). While both flags are true the web client's Equipment page shows a banner with a Reset now button. In host code the flag is IsSetupEquipmentDirty, and SetupEquipmentDirtyChanged is raised on each change.

An edit made through a child key marks nothing. StaticTranslation/UpdateTrans, StlFile/UpdateSource, Box3d/Update, a cylindroid's pair updates and every other per-kind route change the setup object in place. The change reaches the runtime copy at the next fresh session and not before: a play started on a session that is still open — a second start with no Reset in between — runs against the old placement or the old shape, and nothing in its messages says so. The setup canvas draws the workpiece's raw and target solids from the runtime copy, so a nested edit to either geometry goes on showing the old shape there; the anchors, the fixture and the machine it draws from the setup itself.

After the last nested edit inside a slot or a branch, re-install the slot's own object — posting the same key again is enough — and clear its cached solid where it has one:

Edited inside Re-install Then
Workpiece raw geometry POST /api/Workpiece/UpdateRawGeometry?geometryKey=<slot key> POST /api/Workpiece/ClearRawGeomCache
Workpiece target geometry POST /api/Workpiece/UpdateIdealGeometry?geometryKey=<slot key> POST /api/Workpiece/ClearIdealGeomCache
The transformer of a fixture, workpiece or machine-tool branch POST /api/mech/equipment-topology/{owner}/branch/{guid}/update-transformer?transformerKey=<its key> —
The worktable attachment's transformer POST /api/mech/equipment-topology/equipment/branch/{guid}/update-transformer?transformerKey=<its key> —

This is what the browser client does after every nested edit, and it makes the edit effective whatever state the session is in. Resetting before every start is the second guard, and saving and reloading rebuilds the runtime copy as well — one more reason every build ends with save, reload and a read-back.

Running a Replay

A scripted replay is reset, start, then poll by run id:

  1. GET /api/Execution/status. If executionStatus is Running or Paused, a run is live — do not reset, start or stop it; a reset stops a live run too.
  2. POST /api/Execution/reset. Start does not reset. After a finished run the session stays open, holding the machined workpiece, its steps and its messages, and POST /api/Execution/start plays the mission again on top of it. Every Read On First Or Write record then sees steps already produced and writes instead of reading, so the stage-0 record overwrites the cached stock with the machined part — and every later run, reset or not, starts from stock that has already been cut until that file is deleted. Setup edits made through child keys are not picked up by such a play either. Reset before every start. While an NC program plays on the session outside the player's run — a POST /api/SessionShell/PlayNcFile request, for one, which executionStatus does not show — the reset answers 409 { success: false, code: "Reset--Busy", message } and resets nothing: wait for that play to end, then reset. A write under the machine tool's or the fixture's solids answers 409 { success: false, code: "SolidEditRefusedWhileRunning" } during such a play as during a run, only with no banner on the Equipment page.
  3. POST /api/Execution/start, and read the body. An enabled Script command that does not compile refuses the start with HTTP 200, success: false and code: "script_compile_error", naming the script and the error. A start while a run is live — running or paused — answers alreadyRunning: true and starts nothing; the runId beside it is the live run's, and the service cannot tell whether it is yours (a client whose own start answer was lost may compare it with the last runId it knew) — do not poll for it as your own. A start while an NC program plays on the session outside the player's run — a POST /api/SessionShell/PlayNcFile or PlayNc request, for one — answers 409 { success: false, code: "Start--BusyAnotherThread", message } and starts nothing: wait for that play to end, then start. While a project is being changed, or another request's ResetRuntime or EndSession runs, it answers the same with Start--BusyReset; run-line and run-step answer both the same way. A real start answers alreadyRunning: false with the runId of your run and the instanceId of the service process it counts in: keep both. It also says whether the run began on a fresh session. continuesSession: true, with a note saying so, means nothing came between the last play — finished or stopped — and this start to end its session, neither a Reset nor a project load or reload: the run replays the mission on the session that play left, its steps, its messages and the machined workpiece kept. continuesSession: false, with no note, is a start on a fresh session. A true on a replay meant to be fresh says step 2 was skipped: reset, which stops the run, and start again.
  4. Poll GET /api/Execution/status until it reports that runId, under the same instanceId, with executionStatus either Finished or Ready. After a Reset, Ready is how a run that was stopped, or that ended in an exception, reports itself, so a poller that waits for Finished alone never returns from a failed run. Without a Reset in between, such a run inherits the previous run's Finished and passes for a completed one — one more reason to reset before every start. The exception behind a Ready is in the service log, which GET /api/Project/logs returns for the current day. A mission script's own error does not end the run: it is reported as ScriptCommand-Run--Fault in the shell list of GET /api/Execution/messages and the mission goes on, so read that list for a Finished run as well. A higher runId means another caller has started a run since, so this one is over. A different instanceId means the service restarted: a new process counts its runs from 1 again, so the run is gone whatever the runId and the status beside it say.
  5. Read what the run produced before anything resets the session: a Reset empties the step strip, the meshed workpiece and the session's message lists. Check each answer's status, and that it is JSON, before reading it (Reading a Reply): a misspelt evidence route answers 404. The checks that tell a finished run from a passing one are in Replay Acceptance over the HTTP API.

If the poller gives up, let it give up. A timeout is not a reason to call POST /api/Execution/stop: stop ends whatever run is current, which on a shared instance may not be this caller's, and a stopped run skips everything below the point it reached — no Post-Execution output, no end-of-stage record.

Step Color Trees

A script or an agent colors a run's steps by condition under /api/execution/step-color-trees. Three rules keep it from disturbing anyone else on the instance: check and preview a tree before putting it, add a tree with If-None-Match: *, and set the color index — a project setting every connected user sees — only when the tree must show on screen, writing the old key back afterwards while the color index is still the tree's key. The routes, the shape of a tree, the answers and the requests in order are Step Color Trees over HTTP.

Request Encoding

Send request bodies as UTF-8. The service decodes JSON as UTF-8, and a client that encodes the body in another code page loses every character outside that code page before the request leaves the machine — while the call still reports success. The damage shows only when the value is read back.

Windows PowerShell 5.1 is the usual case: Invoke-RestMethod with a string -Body and a content type that carries no charset sends ISO-8859-1, so a group title written in Chinese is saved as ??. Send bytes instead:

Invoke-RestMethod -Method Put -Uri $uri -WebSession $session `
  -Body ([System.Text.Encoding]::UTF8.GetBytes($json)) `
  -ContentType 'application/json; charset=utf-8'

or use an HTTP library that sends UTF-8 by default. The same holds for a program or any other text sent through write-text: what the request body carried is what lands in the file.

See Also