-
Notifications
You must be signed in to change notification settings - Fork 38
API
The backend serves a small REST API for commands and a Socket.IO API for live data, both on port 3001; this page describes both for integrators and frontend developers.
| Part | Details |
|---|---|
| Server | Axum HTTP server in qitech_control/src/api/server.rs, bound to 0.0.0.0:3001
|
| CORS | Permissive: requests from any origin are allowed |
| REST |
POST /api/v1/... with JSON bodies, for commands and setup actions |
| Socket.IO | Same port, namespaces /main and /machine/{vendor}/{machine}/{serial}, MessagePack parser |
| Authentication | None on port 3001. The panel's firewall only lets HTTPS on port 443 through, see Networking-and-Remote-Access. |
Commands go down through REST, state and live values come back through Socket.IO. A REST response only says whether the backend accepted the command; the resulting state arrives as a Socket.IO event.
flowchart LR
C["Client"]:::frontend
R["<a href='https://github.com/qitechgmbh/control/tree/jse-control-v2/qitech_control/src/api/legacy/v1'>REST /api/v1</a>"]:::control
A["<a href='https://github.com/qitechgmbh/control/tree/jse-control-v2/qitech_control/src/api/legacy/adapter'>Machine adapter</a>"]:::control
RT["<a href='https://github.com/qitechgmbh/qitech_framework/tree/main/qitech_framework/src/runtime'>Framework runtime</a>"]:::framework
S["<a href='https://github.com/qitechgmbh/control/tree/jse-control-v2/qitech_control/src/api/legacy/socketio'>Socket.IO</a>"]:::control
C -- "1. POST /machine/mutate" --> R
R -- "2. convert data" --> A
A -- "3. requests, in order" --> RT
R -. "4. success or error" .-> C
RT -- "5. report, every 1/32 s" --> S
S -- "6. StateEvent, LiveValuesEvent" --> C
classDef frontend fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef control fill:#dcfce7,stroke:#15803d,color:#14532d
classDef framework fill:#fef3c7,stroke:#b45309,color:#78350f
classDef lib fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
classDef hardware fill:#f1f5f9,stroke:#475569,color:#0f172a
All routes are POST and live under /api/v1 (v1/mod.rs). There is no route to list machines; use the MachinesEvent on the /main namespace.
| Route | Purpose |
|---|---|
POST /api/v1/machine/mutate |
Send a command to one machine |
POST /api/v1/modbus/scan |
Rescan the USB serial ports |
POST /api/v1/write_modbus_device_assignment |
Assign a USB serial port to a machine, or unassign it |
POST /api/v1/write_machine_device_identification |
Write machine, serial and role into an EtherCAT terminal's EEPROM |
Every route that targets a machine uses the same object, machine_identification_unique. All numbers are decimal u16 values:
{
"machine_identification": { "vendor": 1, "machine": 6 },
"serial": 1
}Vendor 1 is QiTech. Machine IDs and serials are explained in Identification.
Request body (machine_mutate.rs):
| Field | Type | Meaning |
|---|---|---|
machine_identification_unique |
object | The target machine, see above |
data |
JSON value | The machine-specific command |
data depends on the machine type. A command with a value is a JSON object with one key, the command name, for example {"SetTargetDiameter": 1.75}. A command without a value is just its name as a string, for example "GotoTraverseHome" on a winder. The backend picks the adapter for the machine type (api/legacy/adapter), which turns data into one or more framework requests, for example "set config property diameter.target to 1.75". The requests run in order and the first error stops the rest. The machine pages list the commands each machine accepts: Winder, Extruder, Aquapath, Laser.
Example, run on the panel: set the target diameter of Laser V1 with serial 1:
curl -X POST http://localhost:3001/api/v1/machine/mutate \
-H 'Content-Type: application/json' \
-d '{"machine_identification_unique":{"machine_identification":{"vendor":1,"machine":6},"serial":1},"data":{"SetTargetDiameter":1.75}}'Response on success, HTTP 200:
{ "success": true, "error": null }Errors:
| Situation | HTTP status | Body |
|---|---|---|
| The machine type has no adapter | 500 | {"error": "no such machine"} |
data isn't a command of this machine type |
500 |
{"error": "<parser message>"}, for example unknown variant ...
|
| The runtime rejects a request: machine not found, value outside its limits, property not writable | 500 | {"error": "<message>"} |
| The body isn't valid JSON or misses a field | 400 or 422 | Plain text |
Content-Type: application/json is missing |
415 | Plain text |
Rescans the USB serial ports and sends the result as a ModbusDevicesEvent on /main. The request needs no body. The response is always {"success": true, "error": null}.
Assigns a USB serial port to a machine (modbus.rs). This is what Setup → Modbus uses.
{
"port": "<port from ModbusDevicesEvent>",
"device_machine_identification": {
"machine_identification_unique": {
"machine_identification": { "vendor": 1, "machine": 6 },
"serial": 1
},
"slave_id": 1
}
}| Field | Meaning |
|---|---|
port |
The port value of a device in ModbusDevicesEvent (the port's /dev/serial/by-path name) |
device_machine_identification |
The machine and the Modbus slave ID (1 to 254), or null to unassign the port |
The response is {"success": true, "error": null}, or {"success": false, "error": "<message>"} if the file couldn't be written; both with HTTP 200. After saving, the backend sends an updated ModbusDevicesEvent. The assignment takes effect after a backend restart, see Identification.
Writes machine, serial and role into the EEPROM of one EtherCAT terminal. This is what Setup → EtherCat → Assign uses; the procedure is in Identification. The bus must be in PreOp, and the change takes effect after a backend restart.
{
"hardware_identification_ethercat": { "subdevice_index": 4097 },
"device_machine_identification": {
"machine_identification_unique": {
"machine_identification": { "vendor": 1, "machine": 2 },
"serial": 57922
},
"role": 3
}
}| Field | Meaning |
|---|---|
hardware_identification_ethercat.subdevice_index |
The terminal's configured address, as reported in EthercatDevicesEvent
|
device_machine_identification |
The machine and the terminal's role. All zeros unassigns the terminal. |
The response has the same {success, error} shape as a mutation.
The Socket.IO server runs on port 3001 with the default path /socket.io/ and uses the MessagePack parser (socketio/mod.rs), so clients need a MessagePack parser as well. The server only sends; it doesn't handle messages from clients.
Every message uses the Socket.IO event name "event" with this payload (events.rs):
| Field | Type | Meaning |
|---|---|---|
name |
string | The event type, for example MachinesEvent
|
data |
object | The event content |
ts |
integer | Milliseconds since the Unix epoch when the event was created |
/main carries bus and setup information (main_namespace.rs). A client that connects gets the last EthercatStateEvent, EthercatDevicesEvent, ModbusDevicesEvent and MachinesEvent right away.
name |
data |
Sent |
|---|---|---|
EthercatStateEvent |
{"State": "<state>"}, with the state no interface, booting, init, preop, preoppdi, op or lost
|
On every bus state change during start-up, lost when the runtime disconnects |
EthercatDevicesEvent |
{"Done": {"devices": [...]}} with one EtherCAT device per terminal, or {"Error": "<reason>"}
|
After the terminals' identities are read at start-up; Error when the runtime disconnects |
ModbusDevicesEvent |
{"devices": [...]} with one Modbus device per port |
At start-up, after modbus/scan and after write_modbus_device_assignment
|
MachinesEvent |
{"machines": [{"machine_identification_unique": {...}, "error": null}]} |
Whenever a machine is added or removed, always the full list. error holds the build error of a machine that couldn't start. |
| Field | Meaning |
|---|---|
configured_address |
The terminal's configured (station) address |
name |
Terminal name from its EEPROM, for example EL2002
|
vendor_id, product_id, revision
|
The terminal's EtherCAT identity |
device_identification.device_machine_identification |
{machine_identification_unique, role} read from the EEPROM, or null if it couldn't be read. All zeros means unassigned. |
device_identification.device_hardware_identification |
{"Ethercat": {"subdevice_index": <n>}}, where n equals configured_address
|
| Field | Meaning |
|---|---|
port |
The key used for assignments (the port's /dev/serial/by-path name) |
present |
false for an assigned port that isn't plugged in |
device_node, by_id, description
|
Device file, /dev/serial/by-id name and USB description, or null
|
usb_vid, usb_pid, usb_serial
|
USB IDs of the adapter, or null
|
assignment |
{machine_identification_unique, slave_id}, or null if the port isn't assigned |
Each running machine has the namespace /machine/{vendor}/{machine}/{serial} with decimal numbers, for example /machine/1/6/1 for Laser V1 with serial 1 (machine_namespace.rs). The server disconnects clients that connect to a machine that doesn't exist.
name |
data |
Sent |
|---|---|---|
StateEvent |
The machine's full state: set values, modes, limits. Contains is_default_state, which is true only in the first StateEvent after the backend starts. |
On connect, after every mutation and whenever a config or state value changes, with the next report |
LiveValuesEvent |
The machine's measurements, for example diameters or temperatures | Every report, 32 times per second |
The backend keeps the latest values of every machine, so a client that connects late gets the complete state with the next report and doesn't have to wait for a change. The fields of both events are machine-specific; see the machine pages and Data shapes.
A Node.js client in TypeScript, using the same packages as the Electron app (socket.io-client and socket.io-msgpack-parser):
npm install socket.io-client socket.io-msgpack-parserimport { io } from "socket.io-client";
import MsgPackParser from "socket.io-msgpack-parser";
type QiTechEvent = { name: string; data: any; ts: number };
const baseUrl = "http://localhost:3001";
// machine list
const main = io(`${baseUrl}/main`, { parser: MsgPackParser });
main.on("event", (event: QiTechEvent) => {
if (event.name !== "MachinesEvent") return;
for (const m of event.data.machines) {
const { machine_identification: id, serial } = m.machine_identification_unique;
console.log(`${id.vendor}/${id.machine}/${serial}`, m.error ?? "running");
}
});
// one machine: Laser V1, serial 1
const laser = io(`${baseUrl}/machine/1/6/1`, { parser: MsgPackParser });
laser.on("event", (event: QiTechEvent) => {
if (event.name === "StateEvent") console.log("state", event.data);
if (event.name === "LiveValuesEvent") console.log("diameter", event.data.diameter);
});The Electron app does the same in electron/src/client/socketioStore.ts; see Frontend.
Two layers define what a machine exposes:
| Layer | Where | What it defines |
|---|---|---|
| Machine schema |
qitech_control/schemas, one YAML file per machine type |
The typed contract between machine code and framework: identification (vendor_id, machine_id), config properties, state properties, measurements and events, each with its type or unit. !? marks a value that can be missing. |
| API payloads | The adapters in api/legacy/adapter
|
The data commands of machine/mutate and the fields of StateEvent and LiveValuesEvent, built from the schema's properties |
Example, Laser V1: laser_v1.yaml defines the config property diameter.target. The adapter maps the command SetTargetDiameter to it and reports it as laser_state.target_diameter in the StateEvent.
| Schema | Machine |
|---|---|
winder_v1.yaml |
Winder V2 |
winder_v1_7031_0030_spool.yaml |
Winder V2 (EL7031-0030 spool) |
extruder_v1.yaml |
Extruder V2 |
extruder_v2.yaml |
Extruder V3 |
aquapath_v1.yaml |
Aquapath V1 |
laser_v1.yaml |
Laser V1 |
The frontend validates events and commands with Zod schemas that are written by hand, for example laser1Namespace.ts. Nothing generates them, so a change in an adapter has to be made there too.
To reach the API from another computer, over HTTPS with Basic Auth, see Networking-and-Remote-Access.
QiTech Control · GitHub · Framework wiki · Lib wiki · Report a docs problem
Getting Started
Guides
Machines
Developers
Related