@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
JavaScript
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