Skip to content

The file goes to the machine first

The physical worldMQTTartifactrequireArtifactartifactUrlartifactSha256

A 3D printer on the lineStep 2 of 7The file goes to the printer

The boat is a 2.6 MB file on a laptop. The printer will only print what is on its own card.

← A line from the environmentA machine that hosts its own broker →

A printer prints only files on its own storage: upload over implicit FTPS, hash it, and the URL and sha256 become the unit's artifact.

The situation

A machine that runs a program needs the program on the machine before the job starts, and the job should name exactly that file. Built by hand, that is a file share, a naming convention and hope. Here the driver uploads the file, hashes it, and the route carries the URL and the hash on every handoff into a machine.

The setup

A Bambu printer serves its storage over implicit FTPS on port 990, user bblp, the access code as the password. The data channel must reuse the control channel’s TLS session:

the FTPS session, port 990
(TLS from the first byte) ← 220 ready
USER bblp ← 331 Password required
PASS <printer access code> ← 230 Logged in
TYPE I ← 200
PBSZ 0 ← 200
PROT P ← 200
PASV ← 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)
STOR cache/Pmw8druzy.gcode.3mf ← 150
(data channel: TLS to port p1*256+p2, on the control channel's session)
(the file's bytes, then close) ← 226 Transfer complete
QUIT

files.ts speaks it on Node’s tls, then returns the URL a print request names:

examples/workflows/bambu-printer/files.ts
/** Upload `localPath` to the printer's cache as `name`; returns the URL a print request names. */
export async function stageFile(printer: Pick<BambuPrinter, 'host' | 'code'>, localPath: string, name: string): Promise<string> {
const control = new Control(await tlsTo(printer.host, PRINTER_ACCESS.FTPS_PORT));
try {
// 1. Log in on the TLS control channel, protect the data channel
await control.reply();
await control.send(`USER ${PRINTER_ACCESS.USER}`, /^331\b/);
await control.send(`PASS ${printer.code}`, /^230\b/);
await control.send('TYPE I', /^200\b/);
await control.send('PBSZ 0', /^200\b/);
await control.send('PROT P', /^200\b/);
// 2. A passive data channel on the control channel's TLS session
const pasv = await control.send('PASV', /^227\b/);
const [, , , , p1, p2] = pasv.match(/\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)/)!.slice(1).map(Number);
const stored = control.send(`STOR ${PRINTER_ACCESS.CACHE_DIR}/${name}`, /^1\d\d\b/);
const data = await tlsTo(printer.host, p1 * 256 + p2, control.socket.getSession());
await stored;
// 3. Send the file, close the data channel, wait for the printer to confirm
const done = control.reply();
await new Promise<void>((resolve, reject) => data.end(readFileSync(localPath), () => resolve()).on('error', reject));
const line = await done;
if (!/^226\b/.test(line)) throw new Error(`printer FTPS STOR: ${line}`);
return cachedFileUrl(name);
} finally {
control.socket.end('QUIT\r\n');
}
}

The driver puts the URL and the file’s sha256 on the route as its artifact:

scripts/bambu-print.ts
import { UNIT_ROUTE_WORKFLOW, type UnitRouteInput } from '../examples/workflows/machine-line/workflows/route';
import { exitRole, operatorRole } from '../lib/machines/line';
// 1. The artifact, on the printer
const routeKey = `P${Date.now().toString(36)}`;
const sha256 = createHash('sha256').update(readFileSync(file)).digest('hex');
const url = await stageFile(printer, file, `${routeKey}.gcode.3mf`);
// 2. The route: one print step at the station, then a person unloads
const input: UnitRouteInput = {
siteId: line.siteId, lineId: line.lineId, routeKey, orderRef: `ORD-${routeKey}`,
units: [{ unitId: `U-${routeKey}`, carrierId: `PLATE-${routeKey}` }],
steps: [{ machineId: station, process: PRINT_PROCESS, recipe: { ...PRINT_RECIPE } }],
artifact: { url, sha256 }, exitRole: exitRole(line), operatorRole: operatorRole(line),
handoffTimeout: env.BAMBU_HANDOFF_TIMEOUT || '6h',
};
await api('POST', `/workflows/${UNIT_ROUTE_WORKFLOW}/invoke`, { data: input, metadata: { source: 'bambu-print' }, workflowId });

On the floor

The handoff into the printer carries artifactUrl and artifactSha256 in its metadata. The codec sends the URL in project_file as file:///sdcard/cache/<name>. The printer writes to its card slowly, so the upload takes a minute or more before the printer confirms with 226.

The guarantee

With requireArtifact: true, the gateway refuses to dispatch a handoff into the machine that names no file. The artifact on the handoff is the file the job ran, by URL and by hash. See Machines.