Skip to main content

Write a machine driver

A machine connection sends a file to a machine's manager and tracks the job, or fires a command at an actuator. When the manager speaks plain HTTP (OctoPrint, Duet, Moonraker, PrusaLink), you teach Cobblr to drive it with a driver manifest: JSON describing the calls. The declarative engine interprets one manifest into a working driver, so a new machine manager needs no platform deploy.

Machine connections live inside the Digital Fabrication module, which ships as Experimental; see the status note at the top of Digital fabrication for what that means.

For a manager that isn't reachable over HTTP (MQTT, a binary protocol, a handshake), the manifest can't express it. Put an adapter next to the hardware instead, covered under when a manifest isn't enough.

A fabrication driver

This is the built-in OctoPrint driver, which is a good template for any manager that takes a file and runs it:

{
"id": "octoprint",
"name": "OctoPrint",
"version": "1.0.0",
"auth": { "kind": "header", "header": "X-Api-Key", "from": "apiKey" },
"routing": false,
"test": { "method": "GET", "path": "/api/version" },
"listDevices": {
"method": "GET",
"path": "/api/printer",
"result": "single",
"map": { "id": "='octoprint'", "name": "='OctoPrint'", "state": "$.state.text" }
},
"upload": {
"method": "POST",
"path": "/api/files/local",
"body": "multipart",
"fileField": "file",
"result": { "fileId": "$.files.local.name" }
},
"submit": {
"method": "POST",
"path": "/api/files/local/{fileId}",
"body": { "command": "select", "print": true },
"result": { "jobId": "={fileId}", "queued": "='true'" }
},
"status": {
"method": "GET",
"path": "/api/job",
"result": {
"state": {
"from": "$.state",
"map": {
"Operational": "completed",
"Printing": "printing",
"Paused": "paused",
"Error": "failed",
"Offline": "failed"
}
},
"progress": "$.progress.completion"
}
}
}

The sections map onto what Cobblr does with a machine:

  • auth: a header carrying a connection credential. from picks which stored field (apiKey, username, or password); prefix is prepended, so a token goes out as Authorization: Bearer <token> while the user pastes only the token.
  • test: a liveness read for the Test button.
  • listDevices: the printer list for the one-time mapping between a Cobblr machine and the manager's device. result: "single" means the response is one device; "array" reads a list at arrayPath.
  • upload: how the sliced file's bytes are sent. "multipart" posts a form part named fileField; "raw" makes the file bytes the request body, which is what Duet and PrusaLink want (the filename then rides in the path via {filename}).
  • submit: start the uploaded file. {fileId} in the path and body is filled from the upload result.
  • status: poll the job. state.from reads the manager's status string and state.map translates its vocabulary into Cobblr's job states (queued, printing, paused, completed, failed, cancelled).

The values are the same expression language as everywhere else: "$.a.b" reads a path, "='lit'" is a literal, "={var}" fills a template var. The template vars available in paths and submit.body strings are fileId, jobId, deviceId, tag, and filename.

For a manager that reports status as plain text rather than JSON (a GRBL <Idle|MPos:...> report, for instance), set status.parse to "text" and make from and progress regexes whose first capture group is the value.

An actuator driver

A driver can carry commands instead of (or alongside) the fabrication sections. A command maps a name to one outbound request, filled from the wire's per-entity args, and returns an ack. No file, no job to poll. This is the Home Assistant driver, which is commands-only:

{
"id": "home-assistant",
"name": "Home Assistant",
"version": "1.0.0",
"auth": { "kind": "header", "header": "Authorization", "from": "apiKey", "prefix": "Bearer " },
"test": { "method": "GET", "path": "/api/" },
"listDevices": {
"method": "GET",
"path": "/api/",
"result": "single",
"map": { "id": "='home-assistant'", "name": "='Home Assistant'" }
},
"commands": {
"run-zone": {
"method": "POST",
"path": "/api/services/script/turn_on",
"body": {
"entity_id": "script.water_zone",
"variables": { "zone": "{zone}", "seconds": "{seconds}" }
}
}
}
}

The {zone} and {seconds} placeholders in path and body are filled from the command's params, which a wire supplies from an entity's fields. A wire reaches a command through the digifab:run-command action.

When a manifest isn't enough

The HTTP manifest covers a REST manager. Two other paths handle the rest:

  • A machine on a private network Cobblr can't dial. Point a declarative connection at the machine and run the software bridge, or serve the fixed webhook contract yourself. Both are the edge-adapter shape: Cobblr only ever makes an HTTP call to a URL you control. See edge-bridge drivers and the edge-adapter contract.
  • A protocol no manifest can express (MQTT, binary, a login handshake). Write an edge-bridge driver, which is code that runs where the machine is and translates the fixed contract into the real protocol.

For the cheapest possible actuator or sensor (a relay, a valve, a scale), the edge firmware turns an ESP32 into a connector that speaks the same contract with no separate host.

To ship your driver as a built-in that every workspace can pick, register it in the drivers catalog; see the contribution guide.