UNPKG

bambu-js

Version:

Tools to interact with Bambu Lab printers

323 lines 12.6 kB
import mqtt from "mqtt"; import EventEmitter from "node:events"; import {} from "./types/printer-state-schema.js"; import { isSupportedModel, getSupportedModels, } from "./models/index.js"; import { PrinterError, PrinterConnectionError, PrinterTimeoutError, PrinterValidationError, PrinterCommunicationError, PrinterSubscriptionError, PrinterReconnectionError, PrinterDisconnectionError, } from "./utilities/printer-errors.js"; /** * Represents the connection state of the printer controller. */ var ConnectionState; (function (ConnectionState) { ConnectionState["DISCONNECTED"] = "disconnected"; ConnectionState["CONNECTING"] = "connecting"; ConnectionState["CONNECTED"] = "connected"; ConnectionState["DISCONNECTING"] = "disconnecting"; })(ConnectionState || (ConnectionState = {})); /** * Controller for monitoring and managing Bambu Lab printers. */ export class PrinterController extends EventEmitter { host; accessCode; serial; model; mqttClient = null; connectionState = ConnectionState.DISCONNECTED; options; connectionTimeout = null; reconnectTimeout = null; constructor(host, accessCode, serial, model, options = {}) { super(); // Validate input parameters if (!host || typeof host !== "string") { throw new PrinterValidationError("Host must be a non-empty string"); } if (!accessCode || typeof accessCode !== "string") { throw new PrinterValidationError("Access code must be a non-empty string"); } if (!serial || typeof serial !== "string") { throw new PrinterValidationError("Serial must be a non-empty string"); } if (!isSupportedModel(model)) { throw new PrinterValidationError(`Unsupported printer model: ${model}. Supported models: ${getSupportedModels().join(", ")}`); } this.host = host; this.accessCode = accessCode; this.serial = serial; this.model = model; this.options = { connectionTimeout: options.connectionTimeout ?? 10000, autoReconnect: options.autoReconnect ?? false, reconnectDelay: options.reconnectDelay ?? 5000, }; } /** * Creates a new printer controller with the specified configuration. * @param config - Configuration object containing printer information. * @returns A new PrinterController instance */ static create(config) { return new PrinterController(config.host, config.accessCode, config.serial, config.model, config.options); } /** * Checks if the controller is currently connected to the printer. */ get isConnected() { return this.connectionState === ConnectionState.CONNECTED; } /** * Gets the current connection state. */ getConnectionState() { return this.connectionState; } /** * Gets the printer model. */ getModel() { return this.model; } /** * Gets the printer host. */ getHost() { return this.host; } /** * Gets the printer access code. */ getAccessCode() { return this.accessCode; } /** * Gets the printer serial number. */ getSerial() { return this.serial; } /** * Connects to the Bambu Lab printer's MQTT broker. * @returns A promise that resolves when the connection is established. */ connect() { return new Promise((resolve, reject) => { if (this.connectionState === ConnectionState.CONNECTED) { resolve(); return; } if (this.connectionState === ConnectionState.CONNECTING) { reject(new PrinterConnectionError("Connection already in progress")); return; } this.connectionState = ConnectionState.CONNECTING; this.clearTimeouts(); // Set connection timeout this.connectionTimeout = setTimeout(() => { this.cleanupConnection(); this.connectionState = ConnectionState.DISCONNECTED; reject(new PrinterTimeoutError("Connection timeout")); }, this.options.connectionTimeout); const mqttUrl = `mqtts://${this.host}:8883`; try { this.mqttClient = mqtt.connect(mqttUrl, { username: "bblp", password: this.accessCode, clientId: `bblp_${Date.now()}`, // Use unique client ID protocol: "mqtts", rejectUnauthorized: false, manualConnect: true, connectTimeout: this.options.connectionTimeout, }); // On successful connection const connectionResolve = () => { this.clearTimeouts(); this.mqttClient?.off("connect", connectionResolve); this.mqttClient?.off("error", connectionReject); this.connectionState = ConnectionState.CONNECTED; // Subscribe to printer reports this.mqttClient?.subscribe(`device/${this.serial}/report`, (err) => { if (err) { this.emit("error", new PrinterSubscriptionError(`Failed to subscribe to printer reports: ${err.message}`, err)); } }); // Emit connect event this.emit("connect"); resolve(); }; // On error during connection const connectionReject = (error) => { this.clearTimeouts(); this.mqttClient?.off("connect", connectionResolve); this.mqttClient?.off("error", connectionReject); this.cleanupConnection(); this.connectionState = ConnectionState.DISCONNECTED; if (this.options.autoReconnect) { this.scheduleReconnect(); } reject(new PrinterConnectionError(`Connection failed: ${error.message}`, error)); }; // Add event listeners for connection phase only this.mqttClient.on("connect", connectionResolve); this.mqttClient.on("error", connectionReject); // Add primary event listeners for ongoing operation this.mqttClient.on("message", this.onMessage.bind(this)); this.mqttClient.on("error", this.onError.bind(this)); this.mqttClient.on("disconnect", this.onDisconnect.bind(this)); this.mqttClient.on("end", this.onEnd.bind(this)); // Start the connection this.mqttClient.connect(); } catch (error) { this.clearTimeouts(); this.connectionState = ConnectionState.DISCONNECTED; reject(error instanceof PrinterError ? error : new PrinterConnectionError(`Unknown connection error: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined)); } }); } /** * Disconnects from the printer's MQTT broker and ends the connection. */ async disconnect() { if (this.connectionState === ConnectionState.DISCONNECTED) { return; } if (this.connectionState === ConnectionState.DISCONNECTING) { return; } this.connectionState = ConnectionState.DISCONNECTING; this.clearTimeouts(); if (!this.mqttClient) { this.connectionState = ConnectionState.DISCONNECTED; return; } try { await this.mqttClient.endAsync(false, {}); } catch (error) { // Emit error but continue cleanup this.emit("error", new PrinterDisconnectionError(`Error during disconnect: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined)); } finally { this.cleanupConnection(); this.connectionState = ConnectionState.DISCONNECTED; } } /** * Send a command to the printer. * @param payload - The payload to send to the device. */ async sendCommand(payload) { if (!this.isConnected || !this.mqttClient) { throw new PrinterConnectionError("Not connected to printer"); } if (!payload || typeof payload !== "object") { throw new PrinterValidationError("Payload must be a valid object"); } try { await this.mqttClient.publishAsync(`device/${this.serial}/request`, JSON.stringify(payload)); } catch (error) { throw new PrinterCommunicationError(`Failed to send command: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined); } } /** * Handle an incoming message. * @param topic - The topic the message was sent to. * @param message - The message payload. */ onMessage(topic, message) { try { const payload = JSON.parse(message.toString()); if (topic === `device/${this.serial}/report`) { this.emit("report", payload); } } catch (error) { this.emit("error", new PrinterCommunicationError(`Failed to parse message: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined)); } } /** * Handles errors that occur after connection is established. * @param error - The error that occurred. */ onError(error) { // Only handle errors if we're connected (avoid handling connection errors twice) if (this.connectionState === ConnectionState.CONNECTED) { // Wrap MQTT errors in our custom error types const wrappedError = error instanceof PrinterError ? error : new PrinterError(`MQTT error: ${error.message}`, error); this.emit("error", wrappedError); } } /** * Handles printer disconnection. */ onDisconnect() { const wasConnected = this.connectionState === ConnectionState.CONNECTED; this.connectionState = ConnectionState.DISCONNECTED; // Only emit disconnect if we were actually connected if (wasConnected) { this.emit("disconnect"); if (this.options.autoReconnect) { this.scheduleReconnect(); } } } /** * Handles printer end event. */ onEnd() { this.cleanupConnection(); this.connectionState = ConnectionState.DISCONNECTED; this.emit("end"); } /** * Clears all active timeouts. */ clearTimeouts() { if (this.connectionTimeout) { clearTimeout(this.connectionTimeout); this.connectionTimeout = null; } if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; } } /** * Cleans up the MQTT connection and removes all event listeners. */ cleanupConnection() { if (this.mqttClient) { this.mqttClient.removeAllListeners(); this.mqttClient = null; } this.clearTimeouts(); } /** * Schedules an automatic reconnection attempt. */ scheduleReconnect() { if (this.reconnectTimeout) { return; // Reconnect already scheduled } this.reconnectTimeout = setTimeout(async () => { this.reconnectTimeout = null; if (this.connectionState === ConnectionState.DISCONNECTED) { try { await this.connect(); } catch (error) { const wrappedError = error instanceof PrinterError ? error : new PrinterReconnectionError(`Reconnection failed: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined); this.emit("error", wrappedError); } } }, this.options.reconnectDelay); } } //# sourceMappingURL=printer-controller.js.map