Skip to content
Christian edited this page Sep 29, 2026 · 3 revisions

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.

Overview

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
Loading

REST API

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

Machine identification

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.

POST /api/v1/machine/mutate

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

POST /api/v1/modbus/scan

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}.

POST /api/v1/write_modbus_device_assignment

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.

POST /api/v1/write_machine_device_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.

Socket.IO

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 namespace

/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.

EtherCAT device

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

Modbus device

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

Machine namespaces

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.

Client example

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-parser
import { 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.

Data shapes

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.

Clone this wiki locally