@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.
271 lines (270 loc) • 9.62 kB
JavaScript
import "../printer-api.interface.js";
import { ExternalServiceError } from "../../exceptions/runtime.exceptions.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 { HttpStatusCode } from "../../constants/http-status-codes.constants.js";
import { WebsocketAdapter } from "../../shared/websocket.adapter.js";
//#region src/services/octoprint/octoprint-websocket.adapter.ts
const WsMessage = {
WS_OPENED: "WS_OPENED",
WS_CLOSED: "WS_CLOSED",
WS_ERROR: "WS_ERROR",
API_STATE_UPDATED: "API_STATE_UPDATED",
WS_STATE_UPDATED: "WS_STATE_UPDATED"
};
const OctoPrintMessage = {
connected: "connected",
reauthRequired: "reauthRequired",
current: "current",
history: "history",
event: "event",
plugin: "plugin",
timelapse: "timelapse",
slicingProgress: "slicingProgress"
};
const octoPrintEvent = (event) => `octoprint.${event}`;
var OctoprintWebsocketAdapter = class OctoprintWebsocketAdapter extends WebsocketAdapter {
printerType = 0;
printerId;
stateUpdated = false;
stateUpdateTimestamp = null;
socketState = SOCKET_STATE.unopened;
apiStateUpdated = false;
apiStateUpdateTimestamp = null;
apiState = API_STATE.unset;
lastMessageReceivedTimestamp = null;
reauthRequired = false;
reauthRequiredTimestamp = null;
login;
logger;
socketURL;
sessionDto;
username;
refreshPrinterCurrentInterval;
constructor(loggerFactory, octoprintClient, eventEmitter2, configService) {
super(loggerFactory);
this.octoprintClient = octoprintClient;
this.eventEmitter2 = eventEmitter2;
this.configService = configService;
this.logger = loggerFactory(OctoprintWebsocketAdapter.name);
}
get _debugMode() {
return this.configService.get(AppConstants.debugSocketStatesKey, AppConstants.defaultDebugSocketStates) === "true";
}
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 this.reauthRequired;
}
isClosedOrAborted() {
return this.socketState === SOCKET_STATE.closed || this.socketState === SOCKET_STATE.aborted;
}
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 ?? "/") + "sockjs/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.refreshPrinterCurrentInterval);
super.close();
}
async sendThrottle(throttle = AppConstants.defaultSocketThrottleRate) {
return await this.sendMessage(JSON.stringify({ throttle }));
}
async reauthSession() {
this.logger.log("Sending reauthSession");
await this.setupSocketSession();
await this.sendAuth();
this.resetReauthRequired();
}
/**
* Retrieve session token by authenticating with OctoPrint API
*/
async setupSocketSession() {
this.resetSocketState();
this.sessionDto = await this.octoprintClient.login(this.login).then((d) => {
const r = d.data;
if (r.name === "_api") {
this.setApiState("globalKey");
this.setSocketState("aborted");
throw new ExternalServiceError("Global API Key detected, aborting socket connection", "OctoPrint");
} else if (r.needs?.group[0] === "guests") {
this.logger.warn("Detected group guests in OctoPrint login response, marking as unauthorized");
this.setApiState("authFail");
this.setSocketState("aborted");
throw new ExternalServiceError("Guest group detected, authentication failed, aborting socket connection", "OctoPrint");
}
this.setApiState("responding");
this.setSocketState("opening");
return r;
}).catch((e) => {
this.setSocketState("aborted");
if (e instanceof ExternalServiceError) {
this.logger.warn(`Printer authorization error, apiState: ${this.apiState}`);
throw e;
} else {
if (e?.response?.status === 403) {
this.setApiState("authFail");
this.setSocketState("aborted");
throw new ExternalServiceError(e, "OctoPrint");
}
this.logger.error(`Printer (${this.printerId}) network or transport error, marking it as unreachable; ${e}`);
this.setApiState("noResponse");
}
throw e;
});
this.username = await this.octoprintClient.getAdminUserOrDefault(this.login).catch((e) => {
const status = e.response?.status;
this.setApiState("authFail");
this.setSocketState("aborted");
if (status && [
HttpStatusCode.BAD_GATEWAY,
HttpStatusCode.NOT_IMPLEMENTED,
HttpStatusCode.SERVICE_UNAVAILABLE,
HttpStatusCode.GATEWAY_TIMEOUT
].includes(status)) this.logger.error(`Detected a 501-504 error (${status}) probably OctoPrint has crashed or is restarting`);
throw e;
});
await this.updateCurrentStateSafely();
this.logger.log(`Setting up printer current interval loop with 10 seconds interval`);
if (this.refreshPrinterCurrentInterval) clearInterval(this.refreshPrinterCurrentInterval);
this.refreshPrinterCurrentInterval = setInterval(async () => {
await this.updateCurrentStateSafely();
}, 1e4);
}
setReauthRequired() {
this.reauthRequired = true;
this.reauthRequiredTimestamp = Date.now();
}
resetReauthRequired() {
this.reauthRequired = false;
this.reauthRequiredTimestamp = null;
}
resetSocketState() {
this.setSocketState("unopened");
this.setApiState("unset");
}
emitEventSync(event, payload) {
if (!this.eventEmittingAllowed) return;
this.eventEmitter2.emit(octoPrintEvent(event), {
event,
payload,
printerId: this.printerId,
printerType: 0
});
}
async afterOpened(_) {
this.setSocketState("opened");
await this.sendAuth();
await this.sendThrottle(AppConstants.defaultSocketThrottleRate);
}
async onMessage(message) {
this.lastMessageReceivedTimestamp = Date.now();
if (this.socketState !== SOCKET_STATE.authenticated) this.setSocketState("authenticated");
const data = JSON.parse(message);
const eventName = Object.keys(data)[0];
const payload = data[eventName];
if (this._debugMode) this.logger.log(`RX Msg ${eventName} ${message.substring(0, 140)}...`);
if (eventName === OctoPrintMessage.reauthRequired) {
this.logger.log("Received 'reauthRequired', acting on it");
this.setReauthRequired();
}
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");
}
/**
* Re-fetch the printer current state without depending on Websocket
* @private
*/
async updateCurrentStateSafely() {
try {
const current = await this.octoprintClient.getPrinterCurrent(this.login, true);
const isOperational = current.data?.state?.flags?.operational;
let job = {};
if (isOperational) job = (await this.octoprintClient.getJob(this.login)).data;
this.setApiState(API_STATE.responding);
return await this.emitEvent("current", {
...current.data,
progress: job?.progress,
job: job?.job
});
} catch (e) {
if (e.isAxiosError) {
const castError = e;
if (castError?.response?.status == 409) {
this.logger.error(`Printer current interval loop error`);
await this.emitEvent("current", { state: {
flags: {
operational: false,
error: false
},
text: "USB disconnected",
error: castError?.response.data.error
} });
return;
}
this.logger.error(`Could not update Octoprint current due to a request error`);
this.setApiState(API_STATE.noResponse);
return;
}
this.logger.error(`Could not update Octoprint current due to an unknown error`);
this.setApiState(API_STATE.noResponse);
}
}
async emitEvent(event, payload) {
if (!this.eventEmittingAllowed) return;
await this.eventEmitter2.emitAsync(octoPrintEvent(event), {
event,
payload,
printerId: this.printerId,
printerType: 0
});
}
async sendAuth() {
if (!this.sessionDto?.session?.length) throw new Error("Cant send auth, session is unset.");
this.setSocketState(SOCKET_STATE.authenticating);
await this.sendMessage(JSON.stringify({ auth: `${this.username}:${this.sessionDto.session}` }));
}
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) this.logger.warn("Global API Key WS State detected");
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 { OctoPrintMessage, OctoprintWebsocketAdapter, WsMessage, octoPrintEvent };
//# sourceMappingURL=octoprint-websocket.adapter.js.map