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 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 is answered with the application's HTML page, not an error. 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.

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 reliable caller therefore checks the status code, then success, then warning, 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 never stop a run from a timeout (see Running a Replay).

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.

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 or a referenced side file that cannot be read (XmlSource-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. 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). 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).

Every edit under this controller rewires the equipment, and the attachment branches are given new guids each time, so read the graph again before addressing one. 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, 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, 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. 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.
  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. Keep the runId it returns.
  4. Poll GET /api/Execution/status until it reports that runId 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 higher runId means another caller has started a run since, so this one is over.
  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. 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.

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