@vextrix3d/machine-agent 0.0.0-stage → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +74 -0
- package/README.md +530 -2
- package/certs/bambu-ca.pem +218 -0
- package/dist/bootstrap.js +252 -0
- package/dist/camera-bambu.js +92 -0
- package/dist/camera-binaries.js +185 -0
- package/dist/camera-frame-bridge.js +102 -0
- package/dist/camera-gateway.js +201 -0
- package/dist/camera-jpeg-bridge.js +19 -0
- package/dist/camera-jpeg-connection.js +121 -0
- package/dist/camera-relay.js +542 -0
- package/dist/camera-stream-bridge.js +58 -0
- package/dist/camera-stream-connection.js +84 -0
- package/dist/camera-tunnel.js +80 -0
- package/dist/camera-webcam-connection.js +186 -0
- package/dist/camera-webcam.js +156 -0
- package/dist/clients/ApiRequestError.js +7 -0
- package/dist/clients/BambuMqttClient.js +108 -0
- package/dist/clients/MoonrakerClient.js +289 -0
- package/dist/clients/Vextrix3dApiClient.js +121 -0
- package/dist/moonraker-camera-reporter.js +21 -0
- package/dist/print-attempt-synchronizer.js +76 -0
- package/dist/printer-gone.js +26 -0
- package/dist/printer-state-reporter.js +93 -0
- package/dist/types/bambu.types.js +10 -0
- package/dist/types/index.js +2 -0
- package/dist/types/moonraker.types.js +16 -0
- package/dist/utils.js +238 -0
- package/package.json +70 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Vextrix3D Machine Agent Licence
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VEXTRIX3D LTD. All rights reserved.
|
|
4
|
+
|
|
5
|
+
VEXTRIX3D LTD is a company registered in England and Wales under company number 17230746
|
|
6
|
+
("Vextrix3D", "we", "us").
|
|
7
|
+
|
|
8
|
+
1. The Software
|
|
9
|
+
|
|
10
|
+
This licence covers the Vextrix3D Machine Agent ("the Software"): the code, files and
|
|
11
|
+
documentation in this package or repository, apart from the third-party components in
|
|
12
|
+
section 6. The Software is licensed, not sold.
|
|
13
|
+
|
|
14
|
+
2. What you may do
|
|
15
|
+
|
|
16
|
+
Vextrix3D grants you a non-exclusive, non-transferable and revocable licence to install and
|
|
17
|
+
run the Software on machines you control, for the sole purpose of connecting printers and
|
|
18
|
+
other machines to the Vextrix3D service at vextrix3d.co.uk with a machine agent API key that
|
|
19
|
+
Vextrix3D has issued for them, for as long as that key and your Vextrix3D account remain
|
|
20
|
+
active. You may make the copies that installing and running it needs.
|
|
21
|
+
|
|
22
|
+
3. What you may not do
|
|
23
|
+
|
|
24
|
+
Except as this licence allows, or as the law allows you despite it, you may not:
|
|
25
|
+
|
|
26
|
+
a. copy, modify, adapt or translate the Software, or create works derived from it;
|
|
27
|
+
b. publish, distribute, sell, rent, lend or sublicense the Software, or otherwise make it
|
|
28
|
+
available to anyone else;
|
|
29
|
+
c. decompile, disassemble or reverse engineer the Software;
|
|
30
|
+
d. use the Software with any service other than Vextrix3D's, or to build or support a
|
|
31
|
+
service that competes with it;
|
|
32
|
+
e. remove or alter any copyright notice in the Software, or this licence.
|
|
33
|
+
|
|
34
|
+
4. Ownership
|
|
35
|
+
|
|
36
|
+
Vextrix3D owns the Software and every intellectual property right in it. Nothing in this
|
|
37
|
+
licence transfers any of them to you, and every right it does not expressly grant is
|
|
38
|
+
reserved.
|
|
39
|
+
|
|
40
|
+
5. Ending
|
|
41
|
+
|
|
42
|
+
This licence ends automatically if you break any of its terms, or when the API key or your
|
|
43
|
+
Vextrix3D account ends, and Vextrix3D may end it at any time by telling you. When it ends,
|
|
44
|
+
you must stop running the Software and delete every copy of it you hold.
|
|
45
|
+
|
|
46
|
+
6. Third-party components
|
|
47
|
+
|
|
48
|
+
The Software is distributed with components that belong to others, each under its own terms,
|
|
49
|
+
which this licence does not change: the packages it depends on, as listed in package.json,
|
|
50
|
+
and Bambu Lab's certificate authorities in certs/bambu-ca.pem.
|
|
51
|
+
|
|
52
|
+
7. No warranty
|
|
53
|
+
|
|
54
|
+
To the fullest extent the law permits, the Software is provided "as is", without warranty of
|
|
55
|
+
any kind, express or implied, including any warranty of satisfactory quality or fitness for
|
|
56
|
+
a particular purpose. Vextrix3D does not warrant that it is free of errors or that it will
|
|
57
|
+
run without interruption.
|
|
58
|
+
|
|
59
|
+
8. Liability
|
|
60
|
+
|
|
61
|
+
To the fullest extent the law permits, Vextrix3D is not liable for any loss or damage arising
|
|
62
|
+
from the Software or its use, including loss of profit, of data or of production, failed or
|
|
63
|
+
spoiled prints, or damage to a printer or other machine. Nothing in this licence limits or
|
|
64
|
+
excludes any liability that cannot be limited or excluded by law, including liability for
|
|
65
|
+
death or personal injury caused by negligence, or for fraud.
|
|
66
|
+
|
|
67
|
+
Your use of the Vextrix3D service itself is governed by its Terms of Service.
|
|
68
|
+
|
|
69
|
+
9. Law
|
|
70
|
+
|
|
71
|
+
This licence is governed by the law of England and Wales, and the courts of England and Wales
|
|
72
|
+
have exclusive jurisdiction over any dispute about it.
|
|
73
|
+
|
|
74
|
+
Questions about this licence go to Vextrix3D through vextrix3d.co.uk.
|
package/README.md
CHANGED
|
@@ -1,3 +1,531 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Machine Agent
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A lightweight Node.js service that connects a single printer to the Vextrix3D platform: a Bambu Lab printer over its LAN MQTT, or a Klipper printer through Moonraker.
|
|
4
|
+
|
|
5
|
+
The agent connects to the printer, reports printer status to the backend, and synchronizes the currently assigned print attempt. Which client it uses comes from the printer's connection in vextrix3d - Bambu MQTT or Klipper Moonraker.
|
|
6
|
+
|
|
7
|
+
The backend remains the source of truth for printer configuration, production state, and print attempt management.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Secure authentication using Machine Agent API keys
|
|
14
|
+
- Real-time printer monitoring via the Bambu MQTT protocol, or Moonraker's WebSocket API on a Klipper printer
|
|
15
|
+
- Automatic printer state synchronization
|
|
16
|
+
- Current print attempt retrieval
|
|
17
|
+
- Automatic cancellation and failure reporting
|
|
18
|
+
- Batched state reporting to minimize API traffic
|
|
19
|
+
- Automatic reconnection
|
|
20
|
+
- The printer's camera relayed to its page, where ffmpeg is installed - a Bambu printer's, or a Klipper printer's webcam
|
|
21
|
+
- Low bandwidth and minimal backend writes
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Architecture
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Vextrix3D Backend
|
|
29
|
+
│
|
|
30
|
+
│ HTTPS
|
|
31
|
+
▼
|
|
32
|
+
Vextrix3dApiClient
|
|
33
|
+
│
|
|
34
|
+
┌───────────────┴───────────────┐
|
|
35
|
+
│ │
|
|
36
|
+
▼ ▼
|
|
37
|
+
Printer state updates Print attempt APIs
|
|
38
|
+
▲ ▲
|
|
39
|
+
│ │
|
|
40
|
+
└───────────────┬───────────────┘
|
|
41
|
+
│
|
|
42
|
+
┌───────────────┴───────────────┐
|
|
43
|
+
│ │
|
|
44
|
+
BambuMqttClient MoonrakerClient
|
|
45
|
+
│ │
|
|
46
|
+
MQTT (TLS) JSON-RPC (WebSocket)
|
|
47
|
+
│ │
|
|
48
|
+
▼ ▼
|
|
49
|
+
Bambu Lab Printer Klipper (Moonraker)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Components
|
|
55
|
+
|
|
56
|
+
### BambuMqttClient
|
|
57
|
+
|
|
58
|
+
Responsible for communication with the printer.
|
|
59
|
+
|
|
60
|
+
Responsibilities:
|
|
61
|
+
|
|
62
|
+
- establish MQTT connection
|
|
63
|
+
- automatically reconnect
|
|
64
|
+
- subscribe to printer reports
|
|
65
|
+
- ask for the printer's whole status on each connection (`pushall`)
|
|
66
|
+
- merge reports of what changed into the status built up before them
|
|
67
|
+
- pass on status reports only, not the printer's answers to commands
|
|
68
|
+
- parse MQTT messages
|
|
69
|
+
- expose printer events
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### MoonrakerClient
|
|
74
|
+
|
|
75
|
+
Responsible for communication with a Klipper printer, through Moonraker - see [Klipper Moonraker](#klipper-moonraker).
|
|
76
|
+
|
|
77
|
+
Responsibilities:
|
|
78
|
+
|
|
79
|
+
- open Moonraker's WebSocket, with a oneshot token where it asks for an API key
|
|
80
|
+
- identify the agent, and subscribe to the printer objects it reads
|
|
81
|
+
- merge each status update into the status built up before it
|
|
82
|
+
- subscribe again as Klipper restarts and turns ready
|
|
83
|
+
- pass the status on again every 30 seconds while nothing changes, and ask again whether Klipper can give one while it cannot
|
|
84
|
+
- reconnect after 2, 5, 15 and 30 seconds, then once a minute
|
|
85
|
+
- read the slicer's estimate from a file's metadata
|
|
86
|
+
- list the webcams as it connects, and again with each `notify_webcams_changed` - see [A Klipper printer's webcam](#a-klipper-printers-webcam)
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### Printer State Reporter
|
|
91
|
+
|
|
92
|
+
Collects printer status updates and sends them to the backend.
|
|
93
|
+
|
|
94
|
+
Features:
|
|
95
|
+
|
|
96
|
+
- reports immediately when the printer state changes
|
|
97
|
+
- reports progress every 5%
|
|
98
|
+
- periodically refreshes the backend (every 2 minutes by default)
|
|
99
|
+
- merges several status messages into a single API request
|
|
100
|
+
- prevents concurrent requests
|
|
101
|
+
- retries a failed report 30 seconds later, then twice as long each time up to 10 minutes, sending the newest state then, and starts at 30 seconds again once one goes through
|
|
102
|
+
|
|
103
|
+
This significantly reduces unnecessary backend traffic.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### Print Attempt Synchronizer
|
|
108
|
+
|
|
109
|
+
Keeps the backend production state synchronized with printer events.
|
|
110
|
+
|
|
111
|
+
When connected:
|
|
112
|
+
|
|
113
|
+
- loads the currently assigned print attempt
|
|
114
|
+
|
|
115
|
+
When printer state changes:
|
|
116
|
+
|
|
117
|
+
- `FAILED` → fail current print attempt
|
|
118
|
+
- `CANCELLED` → cancel current print attempt
|
|
119
|
+
|
|
120
|
+
A Bambu printer reports a print stopped on purpose as `FAILED` too, so the agent reads it as cancelled when the cancel code comes with it (see [Bambu MQTT](#bambu-mqtt)). Klipper reports it `cancelled` itself.
|
|
121
|
+
|
|
122
|
+
The synchronizer only reacts to state transitions, preventing duplicate API calls from repeated MQTT messages.
|
|
123
|
+
|
|
124
|
+
Every call to the backend - these, the state reports, the camera, and the printer's info as the agent starts - shares one answer to a printer that is gone: see [Fault Tolerance](#fault-tolerance).
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### Vextrix3dApiClient
|
|
129
|
+
|
|
130
|
+
Provides authenticated communication with the backend.
|
|
131
|
+
|
|
132
|
+
The endpoints are Firebase functions, named after their exports and reached under `VEXTRIX3D_FUNCTIONS_URL`:
|
|
133
|
+
|
|
134
|
+
- `GET /getMachineAgentInfo`
|
|
135
|
+
- `POST /getMachineAgentCamera` - the printer's camera tunnel token and relay key; the first call makes the tunnel
|
|
136
|
+
- `POST /reportMachineAgentState`
|
|
137
|
+
- `GET /getMachineAgentPrintAttempt`
|
|
138
|
+
- `POST /cancelMachineAgentPrintAttempt`
|
|
139
|
+
- `POST /failMachineAgentPrintAttempt`
|
|
140
|
+
|
|
141
|
+
Authentication is performed using Machine Agent API keys.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### Camera Relay
|
|
146
|
+
|
|
147
|
+
Runs go2rtc beside the printer, and the printer's Cloudflare Tunnel, so that its page in vextrix3d can show the camera live - see [Camera relay](#camera-relay). Where ffmpeg is installed: the relay reads the RTSP stream a Bambu printer serves, or the JPEG frames an A1's or P1's camera sends - see [An A1 or a P1](#an-a1-or-a-p1) - or a Klipper printer's first webcam in Moonraker - see [A Klipper printer's webcam](#a-klipper-printers-webcam).
|
|
148
|
+
|
|
149
|
+
- fetches the builds of go2rtc and cloudflared it pins into `camera/`, from their GitHub releases, each checked against its SHA-256 before it is made executable, and keeps them there from one start to the next
|
|
150
|
+
- writes go2rtc's config itself, into `camera/runtime/`, readable by the agent's user alone, from what the agent already has: the printer's id, access code and serial number from vextrix3d, and the camera's address from the printer's own status (`ipcam.rtsp_url`) - or, for an A1 or a P1, the printer's own address, its camera's frames read on port 6000 and encoded to H.264 - or, for a Klipper printer, its webcam as Moonraker lists it
|
|
151
|
+
- starts go2rtc with the first report of a camera stream - a Bambu printer's status, or Moonraker's list of webcams - starts it again if it stops, and stops it with the agent
|
|
152
|
+
- follows a new camera address without restarting go2rtc, and starts it again at once for a webcam to be read another way
|
|
153
|
+
- runs go2rtc on a pair of ports it finds free, and notes them where the machine's other agents find them
|
|
154
|
+
- takes the machine's camera requests on port 1984, together with every other agent there, and hands each to the go2rtc of the printer it names, once its relay token checks out against that printer's key
|
|
155
|
+
- asks vextrix3d for the printer's camera (`getMachineAgentCamera`) once the printer names a camera stream in its status and go2rtc and cloudflared are there - the relay key, and the token of the printer's own tunnel, which vextrix3d makes the first time - and runs `camera/cloudflared` with it, starting it again if it stops
|
|
156
|
+
- asks again until vextrix3d answers, beside the printer's connection and never holding it up, and then once a day, following a new relay key or tunnel token without a restart
|
|
157
|
+
- stops the camera and the tunnel for good where vextrix3d no longer knows the printer's API key (401) or the printer (410) - as the whole agent stops then
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Printer State
|
|
162
|
+
|
|
163
|
+
The backend receives the following information:
|
|
164
|
+
|
|
165
|
+
- printer state
|
|
166
|
+
- current subtask name, a model on the plate several times named once with how many (`Bracket.stl ×3`)
|
|
167
|
+
- print progress
|
|
168
|
+
- minutes left of the print, as the printer reckons them - or, on a Klipper printer, from the slicer's estimate
|
|
169
|
+
- nozzle temperature
|
|
170
|
+
- bed temperature
|
|
171
|
+
- chamber temperature
|
|
172
|
+
- Wi-Fi signal strength, of a Bambu printer
|
|
173
|
+
- whether the printer has a camera stream - a Bambu printer one it names, a Klipper printer a webcam the agent reads - a change in it sent at once, as a new state is
|
|
174
|
+
- the agent's own version, which the machine's page shows, from 1.1.0
|
|
175
|
+
- last seen timestamp
|
|
176
|
+
|
|
177
|
+
Supported printer states:
|
|
178
|
+
|
|
179
|
+
- preparing
|
|
180
|
+
- running
|
|
181
|
+
- paused
|
|
182
|
+
- finished
|
|
183
|
+
- cancelled
|
|
184
|
+
- failed
|
|
185
|
+
- idle
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Bambu MQTT
|
|
190
|
+
|
|
191
|
+
Bambu publishes nothing on the protocol; the agent follows the community's account of it, [OpenBambuAPI](https://github.com/Doridian/OpenBambuAPI/blob/main/mqtt.md), and the [Home Assistant integration](https://github.com/greghesp/ha-bambulab) where that says nothing.
|
|
192
|
+
|
|
193
|
+
- **Connection** — the printer's own broker at `mqtts://<ip>:8883`, user `bblp`, the LAN access code as the password. Reports arrive on `device/<serial>/report`, commands go to `device/<serial>/request`.
|
|
194
|
+
- **Whole and partial reports** — the printer pushes its status by itself, about once a second, and the agent reads every report. An X1 or a P2S sends its whole status every time, a P1 or an A1 only what changed since its last report, so the agent merges each report into the status built up before it. `pushall` is not how status arrives: the agent asks for the whole status with it once on each connection, to catch up on what was missed while it was down. OpenBambuAPI advises asking for it no oftener than every five minutes on a P1P.
|
|
195
|
+
- **Answers to commands** — the printer answers on the report topic, under the same key as its status (`print`). Only `print.command === "push_status"` is read as status.
|
|
196
|
+
- **Cancelled prints** — a print stopped on purpose ends as `gcode_state: "FAILED"`, the same as a fault, with `print_error` 0300-400C (50348044) for a couple of seconds. The agent reads that failure as cancelled for as long as the printer stays failed. This comes from the Home Assistant integration; OpenBambuAPI does not cover it.
|
|
197
|
+
- **Temperatures under `device`** — the newer printers pack two numbers into one: the target in the high 16 bits, the reading in the low. A bed held at 105° comes as 6881385. The chamber's (`device.ctc.info.temp`) is read the same way; the nozzle's and the bed's are read from `nozzle_temper` and `bed_temper`.
|
|
198
|
+
- **TLS** — capped at 1.2: P2S firmware 01.02.00.00 never answers a TLS 1.3 handshake.
|
|
199
|
+
|
|
200
|
+
The agent checks the printer's certificate, and does not connect to a printer whose certificate fails the check — it logs `MQTT error` with the reason and tries again a minute later:
|
|
201
|
+
|
|
202
|
+
- **Issued by Bambu** — against the CAs in `certs/bambu-ca.pem`. Bambu's own, `BBL CA` and `BBL CA2`, come from OpenBambuAPI's [TLS page](https://github.com/Doridian/OpenBambuAPI/blob/main/tls.md); P2S-01's certificate is issued by `BBL CA`. Some models' certificates are issued through a device CA of the model's, which the printer does not send. The file holds the three the Home Assistant integration ships — `BBL Device CA N7-V2` (some P2S units), `O1C2-V2` (H2C) and `N6-V2` (X2C) — each issued by `BBL CA2`. A model whose device CA is not there yet needs it added.
|
|
203
|
+
- **Issued to the printer** — with the serial number as the TLS server name, since the certificate is issued to the serial and not the IP. A printer saved with a wrong serial number is refused for it.
|
|
204
|
+
|
|
205
|
+
The status also carries what the agent does not report yet: the layer against the total (`layer_num`, `total_layer_num`), and health errors (`hms`). It carries no time a print began (`gcode_start_time` is in OpenBambuAPI's X1 report, not in the P2S's), so the backend keeps when a job began from the reports.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Klipper Moonraker
|
|
210
|
+
|
|
211
|
+
The agent follows Moonraker's own [API documentation](https://moonraker.readthedocs.io/en/latest/external_api/introduction/). The printer is saved in vextrix3d with the Klipper Moonraker connection: its IP, its port and, where Moonraker asks for one, an API key.
|
|
212
|
+
|
|
213
|
+
- **Connection** — `ws://<ip>:<port>/websocket`, the port 7125 unless the install moved it. Moonraker most often trusts the machines of the workshop's own network (`trusted_clients` in `moonraker.conf`), and the printer is then saved with no API key. Where it asks for one, the key is shown once over HTTP, as `X-Api-Key` on `GET /access/oneshot_token`, for a token the WebSocket is opened with - a WebSocket opened from Node sends no headers of its own. Moonraker keeps the key in its own database: it is read at `http://<ip>:7125/access/api_key` from a trusted machine, or with `~/moonraker/scripts/fetch-apikey.sh` on the printer's own computer, and `POST /access/api_key` makes a new one, which the old one stops working for.
|
|
214
|
+
- **Status** — the agent subscribes to `webhooks`, `print_stats`, `display_status`, `extruder` and `heater_bed`, and to a chamber where the printer's config has a sensor, heater or fan named after one (`temperature_sensor chamber`). Moonraker pushes what changed, as `notify_status_update`, and nothing while nothing does.
|
|
215
|
+
- **States** — from `print_stats.state`: `standby` idle, `printing` running, `paused`, `complete` finished, `cancelled`, and `error` failed. Klipper itself shut down - an emergency stop, a heater fault, an MCU lost - or in error is reported failed, whatever the print last said. Klipper has no state before a print starts, so a Klipper printer is never reported preparing.
|
|
216
|
+
- **Klipper restarting** — a `FIRMWARE_RESTART` or a saved config drops the subscription while Moonraker stays up. The agent subscribes again on `notify_klippy_ready`, and asks with each heartbeat while Klipper is starting up.
|
|
217
|
+
- **Time left** — Klipper reckons none. It is the slicer's estimate, `estimated_time` in the file's metadata, less `print_stats.print_duration`, as Fluidd and Mainsail give it; from a file with no estimate, the time printed drawn out over the progress. `print_duration` counts from the first extrusion, so a print still heating up has no time left yet.
|
|
218
|
+
- **Job name** — `print_stats.filename`, without its folder or `.gcode`.
|
|
219
|
+
- **Webcams** — `server.webcams.list` as the agent connects, whatever Klipper's state, and `notify_webcams_changed` as they are changed in Mainsail or Fluidd; a Moonraker from before 2023, which has no list, has none, and the log says it is too old to list them. The first the agent reads is relayed - see [A Klipper printer's webcam](#a-klipper-printers-webcam).
|
|
220
|
+
|
|
221
|
+
Tried against Mainsail's [virtual Klipper printer](https://github.com/mainsail-crew/virtual-klipper-printer) in Docker: the API key, idle, running, cancelled, finished, Klipper shut down, and subscribing again after a host restart.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Request Optimization
|
|
226
|
+
|
|
227
|
+
The agent intentionally minimizes backend traffic.
|
|
228
|
+
|
|
229
|
+
State updates are transmitted only when:
|
|
230
|
+
|
|
231
|
+
- printer state changes
|
|
232
|
+
- progress changes by at least 5%
|
|
233
|
+
- two minutes have elapsed since the previous update
|
|
234
|
+
|
|
235
|
+
If several status messages arrive while a request is in flight, only the newest state is retained and sent afterwards. A report that fails is not sent again with the next status but after a wait - 30 seconds, then twice as long each time, up to 10 minutes - and the newest state is sent then.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Requirements
|
|
240
|
+
|
|
241
|
+
- Node.js 22.12+
|
|
242
|
+
- pm2, on the printer's machine
|
|
243
|
+
- Network access to the printer
|
|
244
|
+
- Machine Agent API key
|
|
245
|
+
- Vextrix3D backend
|
|
246
|
+
- Bambu printer running LAN Mode, or a Klipper printer with Moonraker
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Environment Variables
|
|
251
|
+
|
|
252
|
+
```text
|
|
253
|
+
VEXTRIX3D_FUNCTIONS_URL=https://api.vextrix3d.co.uk
|
|
254
|
+
VEXTRIX3D_MACHINE_AGENT_API_KEY=vx3d-ma.xxxxx.xxxxx
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
In production `VEXTRIX3D_FUNCTIONS_URL` is `https://api.vextrix3d.co.uk`. The API key is the printer's own, made in Vextrix3D under its machine's **Agent API keys**, and shown once.
|
|
258
|
+
|
|
259
|
+
Two files hold them, in the printer's folder the agent runs in, picked by `NODE_ENV` and not by which one happens to exist:
|
|
260
|
+
|
|
261
|
+
| File | Used when |
|
|
262
|
+
| ------------ | --------------------- |
|
|
263
|
+
| `.env.prod` | `NODE_ENV=production` |
|
|
264
|
+
| `.env.local` | anything else |
|
|
265
|
+
|
|
266
|
+
The agent logs which of the two it read at startup, and stops at once in a folder without it.
|
|
267
|
+
|
|
268
|
+
One more is optional: `VEXTRIX3D_CAMERA_BINARIES=manual` leaves the printer's `camera/` alone, for a machine that places its own go2rtc and cloudflared - see [Setting it up](#setting-it-up).
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Deployment
|
|
273
|
+
|
|
274
|
+
It needs Node.js 22.12 or later, which `node --version` prints. Where there is none, or an older one, nvm installs it for the agent's user alone:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Then, in a new terminal:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
nvm install 24
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The agent is installed once on the printer's machine, beside pm2, for every printer the machine runs - with `sudo` in front where npm's global folder is root's, as with a Node from the system's packages rather than nvm:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
npm install -g @vextrix3d/machine-agent pm2
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
machine-agent --version
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
One agent runs one printer, in a folder of its own named for it - `/opt/machine-agents/p2s-01` for a printer named P2S-01 - which holds the printer's `.env.prod`, with its API key, and its `camera/`. The agent reads both from the folder it is started in, and runs under pm2 named for its printer, `machine-agent-p2s-01`, so that the printers of one machine never mix - in `pm2 list`, in the logs, or in a restart. Each printer on the machine is a folder like it.
|
|
297
|
+
|
|
298
|
+
1. **The printer's folder:**
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
sudo mkdir -p /opt/machine-agents/p2s-01 && sudo chown "$USER": /opt/machine-agents /opt/machine-agents/p2s-01
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
cd /opt/machine-agents/p2s-01
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
2. **Its `.env.prod`**, with the key pasted at the prompt and Enter pressed - it stays off the screen and out of the shell's history - then readable by the agent's user alone:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
read -rs KEY && printf 'VEXTRIX3D_FUNCTIONS_URL=https://api.vextrix3d.co.uk\nVEXTRIX3D_MACHINE_AGENT_API_KEY=%s\n' "$KEY" > .env.prod; unset KEY
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
chmod 600 .env.prod
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
3. **Start it** there, `NODE_ENV=production` being what selects `.env.prod`, and save pm2's list, which brings it back after a reboot once `pm2 startup` has set pm2 up to start with the machine:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
NODE_ENV=production pm2 start machine-agent --name machine-agent-p2s-01
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
pm2 save
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
4. **Check it.** The log has `Loaded printer "P2S-01"` and `Connected to "P2S-01"`:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
pm2 logs machine-agent-p2s-01 --lines 40 --nostream
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
pm2 keeps the folder and `NODE_ENV` with the process, so a restart needs neither:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
pm2 restart machine-agent-p2s-01
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
The agent reads the printer's access code from Vextrix3D as it starts, and follows the camera's address as the printer reports it. A new IP address or a regenerated access code goes into the printer's Edit machine dialog in Vextrix3D, and a restart takes it up - for the camera as for the printer's connection. A DHCP reservation for the printer saves the IP address changing.
|
|
341
|
+
|
|
342
|
+
**An update** is the install's, every printer's at once, then a restart of each agent - `/machine-agent-/` names them all:
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
npm install -g @vextrix3d/machine-agent@latest
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
pm2 restart /machine-agent-/
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**One agent per printer, ever.** Never run two agents for one printer, on one computer or two: both would connect the printer's tunnel, Cloudflare would share the camera's requests out between the two computers, and the camera would play only some of the time. When a printer moves to another computer, stop its agent on the old one first, and save pm2's list there so it does not come back after a reboot - then set it up on the new one as above:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
pm2 delete machine-agent-p2s-01 && pm2 save
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Camera relay
|
|
361
|
+
|
|
362
|
+
A printer's page in Vextrix3D can show its camera live - a Bambu printer's, as below, or a Klipper printer's webcam, as under [A Klipper printer's webcam](#a-klipper-printers-webcam). The agent runs [go2rtc](https://github.com/AlexxIT/go2rtc) beside the printer, which reads its RTSPS stream through ffmpeg, and takes the machine's camera requests on `127.0.0.1:1984`. The agent opens the printer's RTSPS itself and checks its certificate as it checks its MQTT's - issued by Bambu, to the printer's serial number - and hands ffmpeg the stream as plain RTSP on a port of this machine alone, so that the check is the same whatever ffmpeg, and whatever OpenSSL, the machine has: OpenSSL 4 matches no name against the common name a Bambu printer's certificate names its serial in, and ffmpeg before 8 checks nothing on RTSPS. A printer whose certificate fails the check is not relayed, and the log says why once, `[camera] The camera of "P2S-01" at 192.168.0.119:322 was not opened: …`. The way from there to the printer's page is the printer's own [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/), which vextrix3d makes for it the first time the agent asks, and which the agent runs with `cloudflared` - there is nothing to set up in Cloudflare.
|
|
363
|
+
|
|
364
|
+
The machine's camera port, 1984, is every agent's on the machine at once - Linux lets the processes of one user hold a port together, and deals the connections out among them. A page's request names its printer, `/api/ws?src=<printer id>`, and whichever agent takes it hands it to that printer's go2rtc, whose port the printer's own agent notes in `~/.local/state/machine-agent/cameras/`. Every printer's tunnel points at that port, so the printers on a machine each run their own tunnel into it.
|
|
365
|
+
|
|
366
|
+
The port lets a request through only with a token vextrix3d's Worker signs for the printer, with the printer's relay key - which vextrix3d gives the printer's agent, and which the agent notes beside its go2rtc's port, readable by the agents' user alone. go2rtc asks no password of a request from this machine, so that check is what keeps the camera to the printer's page: a computer holds the keys of its own printers and nobody else's.
|
|
367
|
+
|
|
368
|
+
Nothing is filled in by hand on the machine. The agent writes go2rtc's config from the printer's id, access code and serial number, which it has from vextrix3d, and the camera's address, which the printer reports in its status - into `camera/runtime/`, readable by the agent's user alone, the access code being in it. It starts go2rtc once the printer names a camera stream, on the first pair of ports it finds free, starts it again if it stops, and stops it with the agent. It does so where ffmpeg is installed, fetching go2rtc and cloudflared into `camera/` itself, and says in its log when it does not and why.
|
|
369
|
+
|
|
370
|
+
ffmpeg is there because the printer stamps every frame of its stream with the same time. go2rtc on its own passes that on, a browser has nothing to pace the frames by, and the picture crawls - about a second of video for every thirty. ffmpeg stamps each frame with the time it arrives and copies the video as it is, which costs next to nothing. The stream has no B-frames, so arrival order is display order.
|
|
371
|
+
|
|
372
|
+
### An A1 or a P1
|
|
373
|
+
|
|
374
|
+
An A1, A1 mini, P1P or P1S has no RTSP, and its status names no `ipcam.rtsp_url` at all. Its camera sends 1280×720 JPEG frames over TLS on port 6000 instead, one every second or two, as [OpenBambuAPI](https://github.com/Doridian/OpenBambuAPI/blob/main/video.md) documents and [Home Assistant's integration](https://github.com/greghesp/ha-bambulab) reads them: the agent sends an 80-byte login first - the size of the rest (64), its type (`0x3000`) and two zeros, each four bytes little-endian, then `bblp` and the access code in 32 bytes each, padded with zeros - and each frame follows as a 16-byte header, whose first four bytes are the JPEG's size, then the JPEG. A wrong access code has the printer close the connection without a frame.
|
|
375
|
+
|
|
376
|
+
The agent knows these models by the first three characters of their serial number - `01S` P1P, `01P` P1S, `030` A1 mini, `039` A1, and `26A` A2L, which Home Assistant's integration reads the same way - and treats one as naming a camera stream from its first status on: each state report says it has one, and it asks vextrix3d for its camera then. It reads the printer at the address its MQTT is reached on.
|
|
377
|
+
|
|
378
|
+
While a page watches, go2rtc's ffmpeg reads the frames from a bridge on this machine alone - an HTTP port the system picks, with a random path written to the camera file alone - as an MJPEG stream (`multipart/x-mixed-replace`, each frame with its length). The bridge opens the printer for each of ffmpeg's requests and closes it with it, so the printer is read only while somebody watches. It checks the printer's certificate as the RTSPS bridge does - issued by Bambu, to the printer's serial number - with TLS held to 1.2, as Home Assistant's integration holds every connection to a printer. A printer that does not prove itself, or refuses the access code, fails ffmpeg's request at once, and the log says why once, `[camera] The camera of "A1-01" at 192.168.0.120:6000 was not opened: …`. The first time frames come, the log says how often and how large, `[camera] "A1-01" sends its camera as a JPEG frame every 1.0s, 51 KB each`.
|
|
379
|
+
|
|
380
|
+
ffmpeg encodes the frames to H.264 - a browser plays no JPEG through Media Source Extensions - with libx264, which Debian's, Ubuntu's, Raspberry Pi OS's and Homebrew's ffmpeg all have; where `ffmpeg -version` shows no `--enable-libx264`, the relay is off and the log says so. It encodes two frames a second, each of the printer's repeated until the next comes: go2rtc gives a frame more than a second after the one before it a millisecond's duration (`pkg/mp4/muxer.go`), and a camera sending one every two seconds then plays as a single still picture. The repeats cost little, being the same picture. `-preset ultrafast -tune zerolatency`, 4:2:0 colour, and a keyframe every two seconds, so that a second page watching starts within two seconds.
|
|
381
|
+
|
|
382
|
+
What it costs, measured against a stand-in printer sending a 1280×720 frame a second: on an Apple M1 Max, ffmpeg takes 1.1% of one core at a frame every two seconds, 1.5% at one a second and 2% at two a second, and the stream about 0.9 Mbit/s at most, every frame of the test picture differing - a printer's picture, mostly still, takes less. On a Raspberry Pi it has not been measured: by the usual gap between the two, expect around 10 to 20% of one of a Pi 4's four cores, and perhaps twice that on a Pi 3 or a Zero 2 W. Nothing runs while nobody watches.
|
|
383
|
+
|
|
384
|
+
None of this has been tried on one of these printers yet - the models, the login and the frames are as the community documents them, and the agent is tested against a stand-in printer that checks the login and sends frames.
|
|
385
|
+
|
|
386
|
+
### A Klipper printer's webcam
|
|
387
|
+
|
|
388
|
+
A Klipper printer's camera is the first of its webcams that the agent reads, as Moonraker lists them (`server.webcams.list`) - the order Mainsail shows them in - set in Mainsail's or Fluidd's settings, or in `moonraker.conf`. Moonraker is asked as the agent connects and tells it of each change (`notify_webcams_changed`), so a webcam added, changed or removed is followed with no restart; one in `moonraker.conf` is read again as Moonraker restarts. Each state report says whether the printer has one, and the agent asks vextrix3d for its camera once it does.
|
|
389
|
+
|
|
390
|
+
An address with no host - Mainsail fills a new webcam in with `/webcam/?action=stream` - is on the page's host, as Mainsail resolves it: the printer's address, on port 80, where nginx hands `/webcam/` to the streamer, or on the port the printer is saved with where that is not a Moonraker's own - 7125, and the 7126, 7127 and on that KIAUH gives a second and third printer's Moonraker on the same computer, all behind the one page. crowsnest's streamer listens on the printer's own computer alone unless its `no_proxy` is set, so nginx is the way to it from another computer. A login in the address is sent as Basic auth, and left out of the log.
|
|
391
|
+
|
|
392
|
+
By the webcam's service:
|
|
393
|
+
|
|
394
|
+
- **MJPEG-Streamer, Adaptive MJPEG-Streamer and UV4L-MJPEG** (`mjpegstreamer`, `mjpegstreamer-adaptive`, `uv4l-mjpeg`) - crowsnest's ustreamer, mjpg-streamer, and what Mainsail gives a new webcam. The agent reads the MJPEG stream, and ffmpeg encodes it to H.264 while somebody watches. A bridge on this machine alone, as for an A1 or a P1, hands ffmpeg no more than 10 frames a second and drops the rest before ffmpeg decodes them. ffmpeg then encodes at a constant 10 frames a second - each frame repeated until the next where the webcam stalls, as go2rtc gives a frame that comes more than a second after the last a millisecond's duration. It scales the picture down to 720 lines where it has more, holds it to 1.5 Mbit/s, and puts a keyframe every two seconds. It turns the picture as the webcam's rotation and flips say, rotated first, as Mainsail and Fluidd turn it on their pages.
|
|
395
|
+
- **WebRTC (camera-streamer)** (`webrtc-camerastreamer`) - camera-streamer, which crowsnest runs for a Raspberry Pi's own camera, and which encodes H.264 on the Pi itself. The webcam's address is its WebRTC page, `/webcam/webrtc`, and ffmpeg reads the H.264 beside it, `video.h264`, and copies it as it comes, which costs next to nothing. Where the webcam is flipped or rotated, which only encoding can do, its MJPEG beside it, `stream`, is encoded as above instead.
|
|
396
|
+
- **Anything else** - go2rtc's, MediaMTX's or Janus's WebRTC, HLS, an iframe - names a page or a player rather than a stream, and is not read.
|
|
397
|
+
|
|
398
|
+
The log says which webcam it relays, and how, `[camera] "Voron-01" relays its webcam "Webcam" (mjpegstreamer-adaptive): its MJPEG is read from http://192.168.0.130/webcam/?action=stream and encoded to H.264 …`. Where there is none, it lists the webcams Moonraker has, `[camera] "Voron-01" has no webcam the agent reads - Moonraker lists "Bed" (webrtc-janus) - …`. The first time frames come, it says how many come and how large, `[camera] The webcam "Webcam" of "Voron-01" at … sends 15.0 frames a second, 40 KB each, of which no more than 10 a second are encoded`. A webcam that cannot be read fails ffmpeg's request at once, and the log says why once, `… was not opened: it answered 404`. A webcam changed to be read another way - camera-streamer where it was MJPEG, or turned - starts go2rtc again on a config for it.
|
|
399
|
+
|
|
400
|
+
What an MJPEG webcam costs, measured with ffmpeg 9 on an Apple M1 Max, for a still scene with sensor noise, 10 frames a second encoded:
|
|
401
|
+
|
|
402
|
+
| Webcam | ffmpeg, of one core | Sent |
|
|
403
|
+
| --- | --- | --- |
|
|
404
|
+
| 640×480 | 2.7% | 0.8 Mbit/s |
|
|
405
|
+
| 1280×720 | 6.1% | 1.6 Mbit/s |
|
|
406
|
+
| 1920×1080, scaled to 720 lines | 8% | 1.0 Mbit/s |
|
|
407
|
+
|
|
408
|
+
Decoding every frame of a webcam sending 30 a second, rather than the bridge's 10, cost 1.7 to 2 times as much. On a Raspberry Pi it has not been measured. By the usual gap between the two, expect a Pi 4 to take 15 to 30% of one of its four cores for crowsnest's default 640×480, and 40 to 80% for a 1080p webcam. That is while somebody watches, and often on the Pi running Klipper itself. A Pi 5 takes about half that, and a Pi 3 about twice.
|
|
409
|
+
|
|
410
|
+
Tried with go2rtc 1.9.14 and ffmpeg 9 against a stand-in ustreamer at 30 frames a second, at 720p and 1080p, rotated and flipped, and stalling for two and a half seconds, and a stand-in camera-streamer's H.264, each played through go2rtc's WebSocket as the page plays it. None of it has been tried with a real Klipper printer: crowsnest, nginx and camera-streamer's `video.h264` are as their sources read.
|
|
411
|
+
|
|
412
|
+
`npm run check:webcam` runs those checks again from a checkout, with go2rtc in `camera/`, ffmpeg and ffprobe installed, and Node 22.18 or later, which runs the script's TypeScript as it is - `-- 1080`, `-- turned`, `-- stall` or `-- h264` for the others - and `-- --url http://voron.local/webcam/?action=stream` reads a real webcam on this computer's network instead, with `--service`, `--rotation`, `--flip-horizontal` and `--flip-vertical` as Moonraker would list it. It says what played: `Played 1280×720, 69 frames over 6.7s, 10.1 a second, the longest step between two 0.10s, …`.
|
|
413
|
+
|
|
414
|
+
### Setting it up
|
|
415
|
+
|
|
416
|
+
In the printer's folder, the agent started there as under [Deployment](#deployment). ffmpeg is all there is to install: the agent fetches go2rtc and cloudflared itself.
|
|
417
|
+
|
|
418
|
+
1. **ffmpeg.** 4.x - Ubuntu 22.04's - works too: the agent finds the version and leaves out the camera file's `option` lines, which it does not read, and the stream starts in about seven seconds rather than three.
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
sudo apt update && sudo apt install -y ffmpeg
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
ffmpeg -version | head -1
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**On macOS** ffmpeg comes from Homebrew. A Mac lets no two processes share a port, so there the machine's first agent takes 1984 to itself, and a second agent's stream arrives through it.
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
brew install ffmpeg
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
2. **Restart the agent.** Finding ffmpeg, and once the printer names a camera stream in its status, it fetches the builds of go2rtc and cloudflared it pins into the folder's `camera/`, from their GitHub releases, each checked against its SHA-256 before it is made executable - then asks vextrix3d for the printer's camera and runs them:
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
pm2 restart machine-agent-p2s-01
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
| `uname -m` | go2rtc v1.9.14 | cloudflared 2026.10.7 |
|
|
441
|
+
| --------------- | ---------------------- | ------------------------------ |
|
|
442
|
+
| Linux `aarch64` | `go2rtc_linux_arm64` | `cloudflared-linux-arm64` |
|
|
443
|
+
| Linux `armv7l` | `go2rtc_linux_arm` | `cloudflared-linux-arm` |
|
|
444
|
+
| Linux `armv6l` | `go2rtc_linux_armv6` | `cloudflared-linux-arm` |
|
|
445
|
+
| Linux `x86_64` | `go2rtc_linux_amd64` | `cloudflared-linux-amd64` |
|
|
446
|
+
| macOS `arm64` | `go2rtc_mac_arm64.zip` | `cloudflared-darwin-arm64.tgz` |
|
|
447
|
+
| macOS `x86_64` | `go2rtc_mac_amd64.zip` | `cloudflared-darwin-amd64.tgz` |
|
|
448
|
+
|
|
449
|
+
The builds and their SHA-256 are pinned in the agent, so a newer go2rtc or cloudflared comes with a newer agent. On 32-bit ARM the agent goes by the ARM version its Node was built for, which `node -p process.config.variables.arm_version` prints - ARMv6's builds for `6`, and ARMv7's for anything else, which it says in its log where that is not `7`. A build already in `camera/` that matches is kept, and one that does not is replaced - by a new file moved over the old one, so that a go2rtc or cloudflared already running goes on running. On macOS the agent unpacks the zip and the tarball with the system's `tar`, and notes the archive each came from beside it - `camera/go2rtc.pin.json` and `camera/cloudflared.pin.json`, with the SHA-256 of the binary it unpacked - to know them again on its next start.
|
|
450
|
+
|
|
451
|
+
A download that fails, or does not match its SHA-256, is deleted unrun and fetched again later - five seconds on, then twice as long each time, up to an hour. The camera waits for both meanwhile, unless `camera/` has a go2rtc and a cloudflared already, which run until the pinned builds replace them. None of it holds up the printer's connection.
|
|
452
|
+
|
|
453
|
+
**A printer naming no camera stream** - its LAN live view off, or a model whose camera the agent does not read - is not relayed, and its page gets no Camera sheet: until its status names one, the agent fetches nothing and asks vextrix3d nothing, as the first ask makes the printer's tunnel and its Camera sheet, which would never play. Its log says so once, `[camera] "P2S-01" reports no camera stream (disable) - its LAN live view is off, or its camera is of a model the agent does not read - so its camera is not relayed until it names one`, with `none` where the printer names nothing at all. An A1 or a P1 names nothing, and is relayed all the same - see [An A1 or a P1](#an-a1-or-a-p1). Turning the printer's LAN live view on starts the camera with the next status, with no restart. Turned off once the camera runs, the relay and tunnel run on, and the printer's Camera sheet says the printer reports no camera stream - each state report says whether it names one.
|
|
454
|
+
|
|
455
|
+
**Placing them by hand.** `VEXTRIX3D_CAMERA_BINARIES=manual` in the printer's `.env.prod` leaves `camera/` alone, for a machine that pins its own: the agent runs whatever executable `camera/go2rtc` and `camera/cloudflared` it finds there, as it does on a machine no build is pinned for - and the relay is off without `camera/go2rtc`, the tunnel without `camera/cloudflared`.
|
|
456
|
+
|
|
457
|
+
3. **Check the log.** An A1 or a P1 has `[camera] "A1-01" is a Bambu A1, whose camera has no RTSP: its JPEG frames are read from 192.168.0.120:6000 and encoded to H.264 while somebody watches` once its first status is in. The first start has `[camera] Fetched go2rtc v1.9.14 into camera/go2rtc (go2rtc_linux_arm64, its SHA-256 checked)` and the same for cloudflared - a later one, finding them in place, says nothing of them. Then `[camera] Taking this machine's camera requests on 127.0.0.1:1984`, `[tunnel] Opening the camera tunnel of "P2S-01"` and cloudflared's own lines, marked `[tunnel]`, with `Registered tunnel connection` once the way to the site is open - the printer's page then has its Camera sheet. Once the printer's first status is in, it has `[camera] Relaying the camera of "P2S-01" through go2rtc on 127.0.0.1:1985, its RTSP on 8555`, then go2rtc's own lines, also marked `[camera]`: `[rtsp] listen addr=127.0.0.1:8555` and `[api] listen addr=127.0.0.1:1985` - this machine only, nothing on the network. A `[camera] Could not fetch` line says why a build could not be fetched, and a `[camera] Relay off` or `[tunnel] Off` line what is missing.
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
pm2 logs machine-agent-p2s-01 --lines 40 --nostream
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
4. **Check the relay**, asking go2rtc for the stream the way a page would - named after the printer's id, which the agent logs as it starts, `Loaded printer "P2S-01" (<id>)`, on the RTSP port of its `Relaying` line, 8555 for the machine's first agent. `codec_name=h264|width=1920|height=1080` is the answer - `width=1280|height=720` from an A1 or a P1, which takes a few seconds longer, its frames coming one every second or two:
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
ffprobe -v error -rtsp_transport tcp -select_streams v -show_entries stream=codec_name,width,height -of compact=p=0 rtsp://127.0.0.1:8555/<printer id>
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Ports
|
|
470
|
+
|
|
471
|
+
All on `127.0.0.1`, none set by hand. Each agent's go2rtc takes the first pair free as it starts, and nothing else on the machine may hold a port in these ranges - `ss -ltn` lists the ports in use:
|
|
472
|
+
|
|
473
|
+
| Ports | What for |
|
|
474
|
+
| ----------- | --------------------------------------------------------------------------------------------------- |
|
|
475
|
+
| 1984 | The machine's camera port, which every agent takes requests on and every printer's tunnel points at |
|
|
476
|
+
| 1985 - 2083 | Each go2rtc's API, which the agents hand the requests to |
|
|
477
|
+
| 8555 - 8653 | Each go2rtc's RTSP, 6570 above its API port, where ffmpeg hands it the stream |
|
|
478
|
+
|
|
479
|
+
The bridge to the printer - an RTSPS stream's, or an A1's or P1's frames - listens on a port of `127.0.0.1` the system picks.
|
|
480
|
+
|
|
481
|
+
Room for 99 printers on a machine. The agents share 1984 as the processes of one user - all of a machine's agents run as the one user, under one pm2; an agent run as another cannot take it, and says so in its log.
|
|
482
|
+
|
|
483
|
+
### Day to day
|
|
484
|
+
|
|
485
|
+
go2rtc starts ffmpeg only while a page watches the camera, about three seconds before the first picture, and stops it when the last page closes. Its lines are in the agent's log, marked `[camera]` - quiet until a page watches, and saying why when the camera cannot be reached - and cloudflared's beside them, marked `[tunnel]`:
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
pm2 logs machine-agent-p2s-01
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Where vextrix3d cannot be asked for the camera, the agent asks again five seconds later, then twice as long each time, up to a minute, for as long as it takes - its log says so the first time, `[camera] vextrix3d did not give this printer its camera`, and again every tenth try. Once it has the camera it asks again every day, and checks the builds in `camera/` with it: a new relay key is taken up with go2rtc running on, and a new tunnel token starts cloudflared again on it - a restart takes either up at once.
|
|
492
|
+
|
|
493
|
+
Each state report is answered with the fingerprint of the printer's relay key as vextrix3d has it - the start of its SHA-256 - which **Reset camera**, or a new camera secret in vextrix3d, changes. Where it is not the fingerprint of the key the camera runs on, the agent asks vextrix3d again there and then, ahead of the daily ask, saying `[camera] vextrix3d names another key for this printer's camera - asking for its new tunnel and key now, ahead of the daily ask` - once for each fingerprint, and again with the next report where the ask fails. So after Reset camera the agent takes up the printer's new tunnel and key within about two minutes, or at once on a restart. cloudflared gives no sign of its own: once its tunnel is taken down it logs nothing and runs on as though connected, until the new token replaces it.
|
|
494
|
+
|
|
495
|
+
Where vextrix3d answers that it no longer knows the printer's API key - unknown, revoked or expired - or the printer itself, deleted, the agent stops the camera and its tunnel for good, with the rest of it - see [Fault Tolerance](#fault-tolerance).
|
|
496
|
+
|
|
497
|
+
The computer's clock has to be right to within about two minutes, as a relay token is good for a minute by the clock of vextrix3d's Worker: keep it set by NTP - systemd-timesyncd or chrony on Linux, where `timedatectl` says `System clock synchronized: yes`, and automatically on macOS. With the clock further off every stream is turned away, and the log has `[camera] A viewer asked for printer … with a relay token that has expired or is not yet valid by this computer's clock`.
|
|
498
|
+
|
|
499
|
+
Each viewer takes about 1 Mbit/s of the workshop's upload, an A1's or P1's camera less. Viewers of one printer share its camera connection here, but each gets a stream of their own through the tunnel, and a page streams only from Watch to Stop, pausing while its tab is hidden.
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Fault Tolerance
|
|
504
|
+
|
|
505
|
+
The agent is resilient to:
|
|
506
|
+
|
|
507
|
+
- temporary network failures
|
|
508
|
+
- printer restarts
|
|
509
|
+
- MQTT reconnects
|
|
510
|
+
- backend restarts
|
|
511
|
+
- transient HTTP failures
|
|
512
|
+
|
|
513
|
+
MQTT reconnects automatically. A failed state report is sent again after a wait - 30 seconds, doubling to 10 minutes - and a print attempt that could not be loaded is loaded again on the next state change. The camera is set up beside the printer's connection, and fetching go2rtc and cloudflared or asking vextrix3d for the camera is tried again until it succeeds.
|
|
514
|
+
|
|
515
|
+
**A printer vextrix3d no longer knows.** Any call answered `401` - the printer's API key unknown, revoked or expired - or `410` - the printer deleted - stops the agent's work for good: state reports, print attempts, the camera and its tunnel, and the printer's connection. The agent says so in one line, `vextrix3d no longer knows this printer's key (401) - stopped; restart the agent once it has a new key`, or `this printer (410)`, and then stays up and idle, asking vextrix3d nothing - rather than exiting, which pm2 would answer by starting it again, over and over. The same goes for the agent's first call, as it starts. Once the printer has a new API key in its `.env.prod`, a restart starts it afresh; for a printer deleted for good, `pm2 delete` its agent.
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Design Goals
|
|
520
|
+
|
|
521
|
+
- one process per printer
|
|
522
|
+
- stateless runtime
|
|
523
|
+
- minimal backend traffic
|
|
524
|
+
- reliable production synchronization
|
|
525
|
+
- automatic recovery from transient failures
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## License
|
|
530
|
+
|
|
531
|
+
Proprietary, © VEXTRIX3D LTD. The agent may be installed and run only to connect printers to Vextrix3D, with an API key Vextrix3D has issued for them - [LICENSE](LICENSE) has the terms.
|