UNPKG

@fdm-monster/server

Version:

FDM Monster is a bulk OctoPrint, Klipper, PrusaLink and BambuLab manager to set up, configure and monitor 3D printers. Our aim is to provide neat overview over your farm.

396 lines (395 loc) 12.3 kB
import "../printer-api.interface.js"; import { SOCKET_STATE } from "../../shared/dtos/socket-state.type.js"; import { API_STATE } from "../../shared/dtos/api-state.type.js"; import { WsMessage } from "../octoprint/octoprint-websocket.adapter.js"; import mqtt from "mqtt"; //#region src/services/bambu/bambu-mqtt.adapter.ts const bambuEvent = (event) => `bambu.${event}`; var BambuMqttAdapter = class BambuMqttAdapter { logger; settingsStore; eventEmitter2; printerType = 3; printerId; socketState = SOCKET_STATE.unopened; apiState = API_STATE.unset; login; lastMessageReceivedTimestamp = null; mqttClient = null; host = null; accessCode = null; serial = null; lastState = null; isConnecting = false; eventsAllowed = true; sequenceIdCounter = 10; isFirstMessage = true; constructor(settingsStore, loggerFactory, eventEmitter2) { this.settingsStore = settingsStore; this.eventEmitter2 = eventEmitter2; this.logger = loggerFactory(BambuMqttAdapter.name); } registerCredentials(socketLogin) { const { printerId, loginDto } = socketLogin; this.printerId = printerId; this.login = loginDto; this.host = loginDto.printerURL?.replace(/^https?:\/\//, ""); this.accessCode = loginDto.password || null; this.serial = loginDto.username || null; } needsReopen() { return this.apiState === API_STATE.responding && (this.socketState === SOCKET_STATE.closed || this.socketState === SOCKET_STATE.error); } needsSetup() { return this.socketState === SOCKET_STATE.unopened; } needsReauth() { return false; } isClosedOrAborted() { return this.socketState === SOCKET_STATE.closed || this.socketState === SOCKET_STATE.aborted; } async reauthSession() { this.logger.debug("reauthSession called but not needed for Bambu"); } open() { if (!this.host || !this.accessCode || !this.serial) throw new Error("Cannot open connection: credentials not registered"); this.connect(this.host, this.accessCode, this.serial); } close() { this.disconnect().catch((err) => { this.logger.error("Error during MQTT disconnect:", err); }); } async setupSocketSession() { if (!this.host || !this.accessCode || !this.serial) { this.updateSocketState(SOCKET_STATE.aborted); this.updateApiState(API_STATE.noResponse); throw new Error("Credentials not properly registered"); } this.updateSocketState(SOCKET_STATE.opening); this.updateApiState(API_STATE.responding); } allowEmittingEvents() { this.eventsAllowed = true; } disallowEmittingEvents() { this.eventsAllowed = false; } connect(host, accessCode, serial) { if (this.mqttClient?.connected) { this.logger.debug("MQTT already connected"); this.updateSocketState(SOCKET_STATE.opened); return; } if (this.isConnecting) { this.logger.warn("Connection already in progress"); return; } this.host = host; this.accessCode = accessCode; this.serial = serial; this.isConnecting = true; this.updateSocketState(SOCKET_STATE.opening); const mqttUrl = `mqtts://${host}:8883`; const timeout = this.settingsStore.getTimeoutSettings().apiTimeout; this.logger.log(`Connecting to Bambu MQTT at ${mqttUrl}`); const connectionTimeout = setTimeout(() => { this.isConnecting = false; this.updateSocketState(SOCKET_STATE.error); this.logger.error("MQTT connection timeout - will keep trying to reconnect"); }, timeout); try { this.mqttClient = mqtt.connect(mqttUrl, { username: "bblp", password: accessCode, reconnectPeriod: 5e3, rejectUnauthorized: false }); this.mqttClient.on("connect", () => { clearTimeout(connectionTimeout); this.isConnecting = false; this.updateSocketState(SOCKET_STATE.authenticated); this.updateApiState(API_STATE.responding); this.logger.log("MQTT connected successfully"); this.logger.debug(`Connected to MQTT broker at mqtts://${host}:8883`); const reportTopic = `device/${serial}/report`; this.mqttClient.subscribe(reportTopic, { qos: 0 }, (err) => { if (err) { this.logger.error(`Failed to subscribe to ${reportTopic}:`, err); this.updateSocketState(SOCKET_STATE.error); } else { this.logger.debug(`Subscribed to ${reportTopic}`); this.sendPushallCommand().catch((err) => { this.logger.error("Failed to send pushall command:", err); }); } }); }); this.mqttClient.on("error", (error) => { this.isConnecting = false; this.updateSocketState(SOCKET_STATE.error); this.emitEvent(WsMessage.WS_ERROR, error.message).catch(() => {}); this.logger.error("MQTT error:", error); }); this.mqttClient.on("message", (topic, message) => { this.lastMessageReceivedTimestamp = Date.now(); this.handleMessage(topic, message); }); this.mqttClient.on("disconnect", () => { this.updateSocketState(SOCKET_STATE.closed); this.emitEvent(WsMessage.WS_CLOSED, "disconnected").catch(() => {}); this.logger.warn("MQTT disconnected"); }); this.mqttClient.on("reconnect", () => { this.updateSocketState(SOCKET_STATE.opening); this.logger.log("MQTT attempting to reconnect..."); this.isFirstMessage = true; }); this.mqttClient.on("close", () => { this.updateSocketState(SOCKET_STATE.closed); this.updateApiState(API_STATE.noResponse); this.emitEvent(WsMessage.WS_CLOSED, "connection closed").catch(() => {}); this.logger.warn("MQTT connection closed - automatic reconnection will be attempted"); if (this.lastState) { const offlineMessage = this.transformStateToCurrentMessage(this.lastState); this.emitEvent("current", { ...offlineMessage, print: this.lastState }).catch(() => {}); } }); this.mqttClient.on("offline", () => { this.updateSocketState(SOCKET_STATE.closed); this.updateApiState(API_STATE.noResponse); this.logger.warn("MQTT client offline - automatic reconnection will be attempted"); if (this.lastState) { const offlineMessage = this.transformStateToCurrentMessage(this.lastState); this.emitEvent("current", { ...offlineMessage, print: this.lastState }).catch(() => {}); } }); } catch (error) { clearTimeout(connectionTimeout); this.isConnecting = false; this.updateSocketState(SOCKET_STATE.error); this.logger.error("Failed to create MQTT client:", error); this.cleanup(); } } async disconnect() { if (!this.mqttClient) { this.updateSocketState(SOCKET_STATE.closed); return; } this.logger.log("Disconnecting MQTT"); this.updateSocketState(SOCKET_STATE.closed); return new Promise((resolve) => { if (this.mqttClient?.connected) this.mqttClient.end(false, {}, () => { this.cleanup(); resolve(); }); else { this.cleanup(); resolve(); } }); } getLastState() { return this.lastState; } async sendPushallCommand() { const payload = { pushing: { sequence_id: this.sequenceIdCounter, command: "pushall", version: 1, push_target: 1 } }; this.logger.debug(`Sending command: ${JSON.stringify(payload)} with sequence ID: ${this.sequenceIdCounter}`); this.sequenceIdCounter++; await this.sendCommand(payload); this.logger.debug("Connected to printer via MQTT"); } async sendCommand(payload) { if (!this.mqttClient?.connected) throw new Error("MQTT not connected"); if (!this.serial) throw new Error("Serial number not set"); const reportTopic = `device/${this.serial}/report`; const message = JSON.stringify(payload); return new Promise((resolve, reject) => { this.mqttClient.publish(reportTopic, message, { qos: 0 }, (err) => { if (err) { this.logger.error("Failed to send command:", err); reject(err); } else { this.logger.debug(`Command sent: ${message}`); resolve(); } }); }); } async startPrint(filename, subtask_name, amsMapping, plateNumber = 1) { await this.sendCommand({ print: { command: "project_file", param: `Metadata/plate_${plateNumber}.gcode`, project_id: "0", profile_id: "0", task_id: "0", subtask_id: "0", subtask_name: subtask_name ?? filename, file: "", url: `file:///${filename}`, md5: "", timelapse: true, bed_type: "auto", bed_levelling: true, flow_cali: true, vibration_cali: true, layer_inspect: true, ams_mapping: amsMapping ? amsMapping.join(",") : "", use_ams: !!amsMapping && amsMapping.length > 0, sequence_id: this.sequenceIdCounter++ } }); } async pausePrint() { await this.sendCommand({ print: { command: "pause", sequence_id: this.sequenceIdCounter++ } }); } async resumePrint() { await this.sendCommand({ print: { command: "resume", sequence_id: this.sequenceIdCounter++ } }); } async stopPrint() { await this.sendCommand({ print: { command: "stop", sequence_id: this.sequenceIdCounter++ } }); } async sendGcode(gcode) { await this.sendCommand({ print: { command: "gcode_line", param: gcode, sequence_id: this.sequenceIdCounter++ } }); } resetSocketState() { this.lastState = null; } updateSocketState(state) { this.socketState = state; this.emitEventSync(WsMessage.WS_STATE_UPDATED, state); } updateApiState(state) { this.apiState = state; this.emitEventSync(WsMessage.API_STATE_UPDATED, state); } async emitEvent(event, payload) { if (!this.eventsAllowed) return; await this.eventEmitter2.emitAsync(bambuEvent(event), { event, payload, printerId: this.printerId, printerType: 3 }); } emitEventSync(event, payload) { if (!this.eventsAllowed) return; this.eventEmitter2.emit(bambuEvent(event), { event, payload, printerId: this.printerId, printerType: 3 }); } transformStateToCurrentMessage(state) { const isPrinting = state.gcode_state === "PRINTING" || state.mc_print_stage === "printing"; const isPaused = state.mc_print_stage === "paused"; const isFailed = state.gcode_state === "FAILED"; const isConnected = this.mqttClient?.connected || false; const hasError = !isConnected || isFailed; if (state.print_error && state.print_error !== 0) this.logger.debug(`Bambu print_error=${state.print_error} gcode_state=${state.gcode_state ?? "?"} (informational only)`); return { state: { text: isConnected ? isFailed ? "Error" : isPrinting ? isPaused ? "Paused" : "Printing" : "Operational" : "Offline", flags: { operational: isConnected && !isFailed, printing: isConnected && isPrinting && !isPaused, paused: isConnected && isPaused, ready: isConnected && !isPrinting && !isFailed, error: hasError, cancelling: false, pausing: false, sdReady: isConnected, closedOrError: !isConnected || isFailed } }, temps: [{ time: Date.now(), tool0: { actual: state.nozzle_temper || 0, target: state.nozzle_target_temper || 0 }, bed: { actual: state.bed_temper || 0, target: state.bed_target_temper || 0 }, chamber: { actual: state.chamber_temper || 0, target: 0 } }], progress: { completion: state.mc_percent || 0, printTime: null, printTimeLeft: state.mc_remaining_time ? state.mc_remaining_time * 60 : null }, job: { file: { name: state.gcode_file || state.subtask_name || null } }, currentZ: state.layer_num || null, offsets: {}, resends: { count: 0, transmitted: 0, ratio: 0 }, logs: [], messages: [] }; } handleMessage(topic, message) { try { const payload = JSON.parse(message.toString()); if (topic.endsWith("/report") && payload.print) { this.lastState = payload.print; if (this.isFirstMessage) { this.logger.debug("Initial message received"); this.isFirstMessage = false; } const combinedPayload = { ...this.transformStateToCurrentMessage(this.lastState), print: payload.print }; this.emitEvent("current", combinedPayload).catch((err) => { this.logger.error("Failed to emit current event:", err); }); } } catch (error) { this.logger.error("Failed to parse MQTT message:", error); } } cleanup() { if (this.mqttClient) { this.mqttClient.removeAllListeners(); this.mqttClient = null; } this.isConnecting = false; this.lastState = null; } }; //#endregion export { BambuMqttAdapter, bambuEvent }; //# sourceMappingURL=bambu-mqtt.adapter.js.map