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.

315 lines (314 loc) 10.9 kB
import "../printer-api.interface.js"; import { httpToWsUrl } from "../../utils/url.utils.js"; import { normalizeUrl } from "../../utils/normalize-url.js"; import { AppConstants } from "../../server.constants.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 { moonrakerEvent } from "./constants/moonraker.constants.js"; import { PP } from "../../utils/pretty-print.utils.js"; import { WebsocketRpcExtendedAdapter } from "../../shared/websocket-rpc-extended.adapter.js"; //#region src/services/moonraker/moonraker-websocket.adapter.ts var MoonrakerWebsocketAdapter = class MoonrakerWebsocketAdapter extends WebsocketRpcExtendedAdapter { printerType = 1; socketState = SOCKET_STATE.unopened; lastMessageReceivedTimestamp = null; stateUpdated = false; stateUpdateTimestamp = null; apiStateUpdated = false; apiStateUpdateTimestamp = null; apiState = API_STATE.unset; login; printerId; refreshPrinterObjectsInterval; printerObjects = { eventtime: null, status: null }; logger; socketURL; constructor(loggerFactory, moonrakerClient, eventEmitter2, configService, serverVersion) { super(loggerFactory); this.moonrakerClient = moonrakerClient; this.eventEmitter2 = eventEmitter2; this.configService = configService; this.serverVersion = serverVersion; this.logger = loggerFactory(MoonrakerWebsocketAdapter.name); } get _debugMode() { return this.configService.get(AppConstants.debugSocketStatesKey, AppConstants.defaultDebugSocketStates) === "true"; } get subscriptionObjects() { return { pause_resume: [], idle_timeout: [], print_stats: [], heaters: [], heater_bed: [], extruder: [], display_status: [], webhooks: [], virtual_sdcard: [], gcode_move: [], stepper_enable: [], fan: [], motion_report: [], system_stats: [] }; } needsReopen() { return false; } needsSetup() { return this.socketState === SOCKET_STATE.unopened; } needsReauth() { return false; } async reauthSession() {} registerCredentials(socketLogin) { const { printerId, loginDto } = socketLogin; this.printerId = printerId; this.login = loginDto; const httpUrlString = normalizeUrl(this.login.printerURL); const httpUrlPath = new URL(httpUrlString).pathname; const wsUrl = httpToWsUrl(httpUrlString); wsUrl.pathname = (httpUrlPath ?? "/") + "websocket"; this.socketURL = wsUrl; } open() { if (this.socket) throw new Error(`Socket already exists by printerId, ignoring open request`); super.open(this.socketURL); } close() { clearInterval(this.refreshPrinterObjectsInterval); super.close(); } async setupSocketSession() { this.resetSocketState(); await this.moonrakerClient.getApiVersion(this.login).catch((e) => { this.setSocketState("aborted"); this.logger.error(`Printer (${this.printerId}) network or transport error, marking it as unreachable; ${e}`); this.setApiState("noResponse"); throw e; }); this.setApiState(API_STATE.responding); await this.updateCurrentStateSafely(); if (this.refreshPrinterObjectsInterval) clearInterval(this.refreshPrinterObjectsInterval); this.refreshPrinterObjectsInterval = setInterval(async () => { await this.updateCurrentStateSafely(); }, 15e3); } emitEventSync(event, payload) { if (!this.eventEmittingAllowed) return; this.eventEmitter2.emit(moonrakerEvent(event), { event, payload, printerId: this.printerId }); } resetSocketState() { this.setSocketState("unopened"); this.setApiState("unset"); } isClosedOrAborted() { return this.socketState === SOCKET_STATE.closed || this.socketState === SOCKET_STATE.aborted; } async afterOpened(_) { this.setSocketState(SOCKET_STATE.opened); const response = await this.sendRequest({ jsonrpc: "2.0", method: "server.connection.identify", params: { client_name: "FDM Monster", version: this.serverVersion, type: "other", url: AppConstants.githubUrl }, id: 1 }); try { const query = this.subscriptionObjects; const result = await this.moonrakerClient.postSubscribePrinterObjects(this.login, response.result.connection_id, query); this.printerObjects = result.data.result; await this.emitCurrentEvent(this.printerObjects); } catch (e) { const ae = e; if (ae.isAxiosError) { if (ae.response?.status === 503) this.logger.warn(`Klipper host issue ${PP(ae.response.data?.error?.message)}`); else if (ae.response?.status === 404) this.logger.error("Error while afterOpened (404) - usually this means Moonraker is still starting"); this.setApiState(API_STATE.noResponse); return; } this.logger.error("Unknown error while afterOpened"); this.setApiState(API_STATE.noResponse); } } async onEventMessage(event) { this.lastMessageReceivedTimestamp = Date.now(); if (this.socketState !== SOCKET_STATE.authenticated) this.setSocketState("authenticated"); const eventName = event.method; if (this._debugMode) this.logger.log(`RX Msg ${eventName} ${JSON.stringify(event.params)?.substring(0, 80)}...`); const payload = event.params?.length ? event.params[0] : void 0; if (eventName === "notify_service_state_changed") { if (!event.params) { this.logger.error("Received 'notify_service_state_changed' but service indicators params were undefined"); return; } const serviceChanged = event.params[0]; if (serviceChanged.klipper?.active_state || serviceChanged.klipper_mcu?.active_state || serviceChanged.moonraker?.active_state) { this.logger.log("Received notify_service_state_changed, reloading Moonraker printer objects"); await this.setupSocketSession(); } return; } if (eventName === "notify_klippy_ready") { this.logger.log("Received notify_klippy_ready, reloading Moonraker printer objects"); return await this.setupSocketSession(); } if (eventName === "notify_klippy_disconnected") { this.logger.log("Received notify_klippy_disconnected, reloading Moonraker printer objects"); return await this.setupSocketSession(); } if (eventName === "notify_klippy_shutdown") { this.logger.log("Received notify_klippy_shutdown, reloading Moonraker printer objects"); return await this.setupSocketSession(); } if (eventName === "notify_status_update") { if (!event.params) { this.logger.error("Received 'notify_status_update' but service indicators params were undefined"); return; } const [data, eventtime] = event.params; const subState = Object.keys(data)[0]; if (Object.keys(this.printerObjects.status).includes(subState)) { this.printerObjects.status = { ...this.printerObjects.status, ...data }; this.printerObjects.eventtime = eventtime; await this.emitCurrentEvent(this.printerObjects); } else this.logger.warn(`Substate ${subState} unknown`); return; } await this.emitEvent(eventName, payload); } async afterClosed(event) { this.setSocketState("closed"); delete this.socket; await this.emitEvent(WsMessage.WS_CLOSED, "connection closed"); } async onError(error) { this.setSocketState("error"); await this.emitEvent(WsMessage.WS_ERROR, error?.length ? error : "connection error"); } async updateCurrentStateSafely() { try { const query = this.subscriptionObjects; const objects = await this.moonrakerClient.getPrinterObjectsQuery(this.login, query); this.printerObjects = objects.data.result; this.setApiState(API_STATE.responding); return await this.emitCurrentEvent(this.printerObjects); } catch (e) { const castError = e; if (castError.isAxiosError) { if (castError?.response?.status == 503) { this.printerObjects.status = null; this.printerObjects.eventtime = Date.now(); return await this.emitCurrentEvent(this.printerObjects); } this.logger.error("Could not update Moonraker printer objects due to a request error"); this.setApiState(API_STATE.noResponse); return; } this.logger.error(`Could not update Moonraker current due to an unknown error`); this.setApiState(API_STATE.noResponse); } } async emitCurrentEvent(printerObject) { const originalKlipperObjects = printerObject.status; const flags = { operational: false, printing: false, cancelling: false, pausing: false, paused: false, resuming: false, finishing: false, closedOrError: false, error: false, ready: false, sdReady: false }; let filename = ""; let printTime = null; let stateText = "Unset"; let error = ""; let completion = null; if (originalKlipperObjects != null) { stateText = originalKlipperObjects.display_status?.message; if (originalKlipperObjects.print_stats?.state?.length) { const systemState = originalKlipperObjects.webhooks; const printState = originalKlipperObjects.print_stats.state; const idleState = originalKlipperObjects.idle_timeout?.state; filename = originalKlipperObjects.print_stats.filename; printTime = originalKlipperObjects.print_stats.print_duration; flags.operational = systemState.state === "ready"; if (flags.operational) { flags.printing = printState === "printing"; flags.paused = printState === "paused"; flags.ready = printState === "standby" && idleState !== "Printing"; flags.sdReady = true; } else { flags.error = true; stateText = "Klipper reports: " + (systemState.state ?? "unknown")?.toUpperCase(); } } completion = (originalKlipperObjects.display_status?.progress ?? 0) * 100; } const currentMessage = { progress: { printTime, completion }, state: { text: stateText, error, flags }, job: { file: { name: filename, path: filename } } }; await this.emitEvent("notify_status_update", originalKlipperObjects); await this.emitEvent("current", currentMessage); } async emitEvent(event, payload) { if (!this.eventEmittingAllowed) return; await this.eventEmitter2.emitAsync(moonrakerEvent(event), { event, payload, printerId: this.printerId, printerType: 1 }); } setSocketState(state) { this.socketState = state; this.stateUpdated = true; this.stateUpdateTimestamp = Date.now(); if (this._debugMode) this.logger.log(`${this.printerId} Socket state updated to: ` + state); this.emitEventSync(WsMessage.WS_STATE_UPDATED, state); } setApiState(state) { if (state === API_STATE.globalKey) throw new Error("GlobalKey is an invalid WS state for Moonraker"); this.apiState = state; this.apiStateUpdated = true; this.apiStateUpdateTimestamp = Date.now(); if (this._debugMode) this.logger.log(`${this.printerId} API state updated to: ` + state); this.emitEventSync(WsMessage.API_STATE_UPDATED, state); } }; //#endregion export { MoonrakerWebsocketAdapter }; //# sourceMappingURL=moonraker-websocket.adapter.js.map