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