From 7ca20074dbe0a40ec7567fa173a8190b1e99c571 Mon Sep 17 00:00:00 2001 From: Ilja Kartashov Date: Wed, 24 Jan 2024 08:42:50 +0100 Subject: [PATCH] Update SLA code to use API v.1 and CONNECT settings --- config.m1.js | 2 +- config.sl1.js | 3 +- src/locales/c.json | 4 + src/locales/source/cs.json | 5 +- src/locales/source/de.json | 5 +- src/locales/source/en.json | 3 + src/locales/source/es.json | 9 +- src/locales/source/fr.json | 7 +- src/locales/source/it.json | 7 +- src/locales/source/kr.json | 7 +- src/locales/source/lt.json | 5 +- src/locales/source/nl.json | 5 +- src/locales/source/pl.json | 5 +- src/locales/source/sk.json | 5 +- src/printer/common.js | 7 +- src/printer/components/cameras.js | 1 - src/printer/components/control.js | 1 - src/printer/components/dataFormat.js | 70 +------ src/printer/components/errors.js | 4 +- src/printer/components/files.js | 5 +- src/printer/components/job.js | 233 +++++++++++----------- src/printer/components/question.js | 2 +- src/printer/components/settings.js | 8 +- src/printer/components/storage.js | 2 +- src/printer/components/toast.js | 4 +- src/printer/{fdm => }/context.js | 227 ++++++++++++++------- src/printer/fdm/index.js | 151 -------------- src/printer/index.js | 173 +++++++++++++++- src/printer/sla/dashboard.js | 26 --- src/printer/sla/index.js | 219 -------------------- src/printer/sla/temperature.js | 13 -- src/printer/{fdm => views}/dashboard.js | 3 - src/printer/{sla => views}/exposure.js | 20 +- src/printer/{sla => views}/refill.js | 4 +- src/printer/{fdm => views}/temperature.js | 0 src/state.js | 18 +- templates/components/job/props.html | 6 +- templates/components/telemetry.html | 12 +- templates/pages/settings.html | 10 +- tools/preprocessing/extract-words.js | 3 +- webpack.config.js | 5 + 41 files changed, 577 insertions(+), 722 deletions(-) rename src/printer/{fdm => }/context.js (57%) delete mode 100644 src/printer/fdm/index.js delete mode 100644 src/printer/sla/dashboard.js delete mode 100644 src/printer/sla/index.js delete mode 100644 src/printer/sla/temperature.js rename src/printer/{fdm => views}/dashboard.js (92%) rename src/printer/{sla => views}/exposure.js (91%) rename src/printer/{sla => views}/refill.js (89%) rename src/printer/{fdm => views}/temperature.js (100%) diff --git a/config.m1.js b/config.m1.js index d321b675..bcad4243 100644 --- a/config.m1.js +++ b/config.m1.js @@ -9,7 +9,7 @@ module.exports = (env, args) => { WITH_STORAGES: ["local", "usb"], WITH_FILES: true, - WITH_SETTINGS: false, + WITH_SETTINGS: true, WITH_CONTROLS: false, WITH_REMOTE_UPLOAD: true, WITH_START_PRINT_AFTER_UPLOAD: true, diff --git a/config.sl1.js b/config.sl1.js index a81518ab..e9fa5931 100644 --- a/config.sl1.js +++ b/config.sl1.js @@ -9,12 +9,13 @@ module.exports = (env, args) => { WITH_STORAGES: ["local", "usb"], WITH_FILES: true, - WITH_SETTINGS: false, + WITH_SETTINGS: true, WITH_CONTROLS: false, WITH_REMOTE_UPLOAD: true, WITH_START_PRINT_AFTER_UPLOAD: true, WITH_LOGS: false, WITH_FONT: false, + WITH_V1_API: true, ...env, }; return webpackConfig(config, args); diff --git a/src/locales/c.json b/src/locales/c.json index b25453db..2ab12e13 100644 --- a/src/locales/c.json +++ b/src/locales/c.json @@ -109,9 +109,13 @@ "msg.sysupgrade.remark": "", "msg.sysupgrade.title": "", "msg.sysupgrade.wait-for-printer": "", + "ntf.calibration-error": "", "ntf.camera-config-success": "", "ntf.camera-suc": "", "ntf.error": "", + "ntf.low-resin.message": "", + "ntf.low-resin.title": "", + "ntf.n-calibrated": "", "ntf.settings-suc": "", "ntf.success": "", "ntf.upld-start": "", diff --git a/src/locales/source/cs.json b/src/locales/source/cs.json index 7a130bb5..cd2d16a0 100644 --- a/src/locales/source/cs.json +++ b/src/locales/source/cs.json @@ -272,5 +272,8 @@ "btn.upgrade": "Aktualizovat", "conn.error_status": "Chyba", "conn.printer.not-supported": "", - "logs.file-size-unknown": "" + "logs.file-size-unknown": "", + "ntf.calibration-error": "", + "ntf.low-resin.message": "", + "ntf.low-resin.title": "" } \ No newline at end of file diff --git a/src/locales/source/de.json b/src/locales/source/de.json index 886fa385..869648a7 100644 --- a/src/locales/source/de.json +++ b/src/locales/source/de.json @@ -286,5 +286,8 @@ "msg.file-exists.overwrite-it": "Möchten Sie sie überschreiben?", "msg.file-exists.title": "Datei existiert bereits", "conn.printer.not-supported": "", - "logs.file-size-unknown": "" + "logs.file-size-unknown": "Die Protokolldatei hat eine unbekannte Größe und ist daher nur zum Herunterladen verfügbar", + "ntf.calibration-error": "", + "ntf.low-resin.message": "", + "ntf.low-resin.title": "" } \ No newline at end of file diff --git a/src/locales/source/en.json b/src/locales/source/en.json index e65af647..96aeded0 100644 --- a/src/locales/source/en.json +++ b/src/locales/source/en.json @@ -105,7 +105,10 @@ "ntf.camera-config-success": "Camera configuration has been updated", "ntf.e-307": "Resin measuring failed!", "ntf.error": "Error", + "ntf.calibration-error": "Calibration Error", "ntf.n-calibrated": "Printer is not calibrated!", + "ntf.low-resin.title": "Resin low", + "ntf.low-resin.message": "Measured resin volume is too low. The print can continue, however, a refill might be required.", "ntf.not-idle": "Printer is not idle!", "ntf.settings-suc": "Settings was changed successfully.", "ntf.start-print": "The printer is getting ready.", diff --git a/src/locales/source/es.json b/src/locales/source/es.json index ef0f342c..8a1a93cb 100644 --- a/src/locales/source/es.json +++ b/src/locales/source/es.json @@ -41,7 +41,7 @@ "ntf.actn-pending": "La acción se realizará al terminar la capa.", "ntf.e-307": "Falló la medición de resina.", "ntf.error": "Error", - "ntf.n-calibrated": "¡La impresora no está calibrada!", + "ntf.n-calibrated": "¡Impresora no calibrada!", "ntf.not-idle": "¡La impresora no está inactiva!", "ntf.start-print": "La impresora se está preparando.", "ntf.success": "Éxito", @@ -249,7 +249,7 @@ "btn.connect.unlink": "Desenlazar", "conn.connect.not-linked": "No enlazado", "msg.drop-zone.label": "Arrastra archivo aquí para subirlo", - "btn.check-updates": "Comprobar actualizaciones", + "btn.check-updates": "Comprobar Actualizaciones", "btn.upgrade": "Actualización", "upgrade.success.message": "La página se recargará en 5 segundos", "upgrade.success.title": "Actualización con Éxito", @@ -267,5 +267,8 @@ "msg.file-exists.overwrite-it": "¿Quieres sobrescribirlo?", "msg.file-exists.title": "El archivo ya existe", "conn.printer.not-supported": "", - "logs.file-size-unknown": "" + "logs.file-size-unknown": "El archivo de registro es de tamaño desconocido y, por lo tanto, solo está disponible para su descarga.", + "ntf.calibration-error": "", + "ntf.low-resin.message": "", + "ntf.low-resin.title": "" } \ No newline at end of file diff --git a/src/locales/source/fr.json b/src/locales/source/fr.json index ab8b7ab5..07894b26 100644 --- a/src/locales/source/fr.json +++ b/src/locales/source/fr.json @@ -80,7 +80,7 @@ "ntf.actn-pending": "L'action sera exécutée une fois la couche terminée.", "ntf.e-307": "La mesure de la résine a échoué", "ntf.error": "Erreur", - "ntf.n-calibrated": "L'imprimante n'est pas calibrée !", + "ntf.n-calibrated": "L'imprimante n'est pas calibrée !", "ntf.not-idle": "L'imprimante n'est pas au repos !", "ntf.settings-suc": "Paramètres modifiés avec succès.", "ntf.start-print": "Préparation de l'imprimante.", @@ -278,5 +278,8 @@ "msg.file-exists.title": "Le fichier existe déjà", "conn.error_status": "Erreur", "conn.printer.not-supported": "", - "logs.file-size-unknown": "" + "logs.file-size-unknown": "Le fichier de journal est d'une taille inconnue et n'est donc disponible que pour le téléchargement", + "ntf.calibration-error": "", + "ntf.low-resin.message": "", + "ntf.low-resin.title": "" } \ No newline at end of file diff --git a/src/locales/source/it.json b/src/locales/source/it.json index f2e01485..f3c2c753 100644 --- a/src/locales/source/it.json +++ b/src/locales/source/it.json @@ -41,7 +41,7 @@ "ntf.actn-pending": "L'azione verrà eseguita dopo il completamento del layer.", "ntf.e-307": "Misurazione resina non riuscita!", "ntf.error": "Errore", - "ntf.n-calibrated": "La stampante adesso è calibrata!", + "ntf.n-calibrated": "La stampante non è calibrata!", "ntf.not-idle": "La stampante non è inattiva!", "ntf.start-print": "Preparazione della stampante.", "ntf.success": "Successo", @@ -267,5 +267,8 @@ "msg.file-exists.overwrite-it": "Vuoi sovrascriverlo?", "msg.file-exists.title": "File già esistente", "conn.printer.not-supported": "", - "logs.file-size-unknown": "" + "logs.file-size-unknown": "Il file di log è di dimensioni sconosciute e quindi è disponibile solo per il download.", + "ntf.calibration-error": "", + "ntf.low-resin.message": "", + "ntf.low-resin.title": "" } \ No newline at end of file diff --git a/src/locales/source/kr.json b/src/locales/source/kr.json index f1c184c4..ed5b023a 100644 --- a/src/locales/source/kr.json +++ b/src/locales/source/kr.json @@ -276,5 +276,8 @@ "msg.file-exists.overwrite-it": "덮어쓰시겠습니까?", "msg.file-exists.title": "파일이 이미 존재합니다.", "conn.printer.not-supported": "지원하지 않는 프린터입니다.", - "logs.file-size-unknown": "로그파일의 크기를 알 수 없으므로 다운로드만 가능합니다." -} + "logs.file-size-unknown": "로그파일의 크기를 알 수 없으므로 다운로드만 가능합니다.", + "ntf.calibration-error": "", + "ntf.low-resin.message": "", + "ntf.low-resin.title": "" +} \ No newline at end of file diff --git a/src/locales/source/lt.json b/src/locales/source/lt.json index 51ea9b4c..4b279067 100644 --- a/src/locales/source/lt.json +++ b/src/locales/source/lt.json @@ -275,5 +275,8 @@ "msg.file-exists.overwrite-it": "", "msg.file-exists.title": "", "conn.printer.not-supported": "", - "logs.file-size-unknown": "" + "logs.file-size-unknown": "", + "ntf.calibration-error": "", + "ntf.low-resin.message": "", + "ntf.low-resin.title": "" } \ No newline at end of file diff --git a/src/locales/source/nl.json b/src/locales/source/nl.json index 12387f72..988f1748 100644 --- a/src/locales/source/nl.json +++ b/src/locales/source/nl.json @@ -275,5 +275,8 @@ "msg.file-exists.overwrite-it": "", "msg.file-exists.title": "", "conn.printer.not-supported": "", - "logs.file-size-unknown": "" + "logs.file-size-unknown": "", + "ntf.calibration-error": "", + "ntf.low-resin.message": "", + "ntf.low-resin.title": "" } \ No newline at end of file diff --git a/src/locales/source/pl.json b/src/locales/source/pl.json index d49119e2..f002d9d5 100644 --- a/src/locales/source/pl.json +++ b/src/locales/source/pl.json @@ -267,5 +267,8 @@ "msg.file-exists.overwrite-it": "Czy chcesz go nadpisać?", "msg.file-exists.title": "Plik już istnieje", "conn.printer.not-supported": "", - "logs.file-size-unknown": "" + "logs.file-size-unknown": "Plik logu ma nieznany rozmiar i dlatego jest dostępny tylko do pobrania", + "ntf.calibration-error": "", + "ntf.low-resin.message": "", + "ntf.low-resin.title": "" } \ No newline at end of file diff --git a/src/locales/source/sk.json b/src/locales/source/sk.json index 85027466..09f09c89 100644 --- a/src/locales/source/sk.json +++ b/src/locales/source/sk.json @@ -278,5 +278,8 @@ "msg.file-exists.overwrite-it": "", "msg.file-exists.title": "", "conn.printer.not-supported": "", - "logs.file-size-unknown": "" + "logs.file-size-unknown": "", + "ntf.calibration-error": "", + "ntf.low-resin.message": "", + "ntf.low-resin.title": "" } \ No newline at end of file diff --git a/src/printer/common.js b/src/printer/common.js index efd67f26..2c008f54 100644 --- a/src/printer/common.js +++ b/src/printer/common.js @@ -2,9 +2,10 @@ import { LinkState, translateState } from "../state"; const SEPARATOR = " - "; -export const getPrinterLabel = (context) => { - return buildTitle([context.printer?.location, context.printer?.name]); -}; +export const getPrinterLabel = (context) => buildTitle([ + context.printer?.location || context.printer?.hostname, + context.printer?.name +]); export const buildTitle = (titleItems) => { return [...titleItems] diff --git a/src/printer/components/cameras.js b/src/printer/components/cameras.js index 9f47cd01..551dc7cb 100644 --- a/src/printer/components/cameras.js +++ b/src/printer/components/cameras.js @@ -411,7 +411,6 @@ const createCameraSettingsModal = (cameraId, resolve) => { inputTriggerScheme.value = translateTriggerScheme(data.trigger_scheme); setVisible(inputFocus.parentNode, hasFocus); - console.log(`DEBUG: has focus (${hasFocus})`, data) if (hasFocus) { inputFocus.value = Math.round(data.focus * 100); } diff --git a/src/printer/components/control.js b/src/printer/components/control.js index cdffac19..382e6458 100644 --- a/src/printer/components/control.js +++ b/src/printer/components/control.js @@ -8,7 +8,6 @@ import updateProperties from "./updateProperties"; import { disableSteppers, extrude, homePrinthead, movePrinthead, retract, setBedTemperature, setFlowRate, setNozzleTemperature, setSpeed } from "./controlActions"; import { handleError } from "./errors"; import { LinkState } from "../../state"; -import { context } from "../fdm"; let moveStep = 1; let extrudeRetractStep = 1; diff --git a/src/printer/components/dataFormat.js b/src/printer/components/dataFormat.js index d0003579..94e08a7e 100644 --- a/src/printer/components/dataFormat.js +++ b/src/printer/components/dataFormat.js @@ -20,19 +20,6 @@ const str_GB = translate("unit.gb"); const str_true = translate("prop.true"); const str_false = translate("prop.false"); -/** - * Format the value data with format specificated. - * @param {string} format - one of ["int", "number", "layer", "temp", "fan", "resin", "cover", "date", "progress", "timeEst", "time", "expo", "boolean"] - * @param {any} value - */ -const formatData = (format, value) => { - if (process.env.PRINTER_TYPE === "sla") { - return slaFormatData(format, value); - } else { - return fdmFormatData(format, value); - } -}; - /** * It formats a number using fixed-point notation with one digit after the decimal point. * ex: 123.456 => 123.4 @@ -193,14 +180,15 @@ function formatBoolean(value) { } /** - * Format the value data with format specificated for sla type. - * @param {string} format - one of ["int", "number", "layer", "temp", "fan", "resin", "cover", "date", "progress", "timeEst", "time", "expo", "boolean"] + * Format the value data with format specificated. + * @param {string} format - one of ["number", "temp", "fan", "pos", "date", "progress", "timeEst", "time"] * @param {any} value */ -const slaFormatData = (format, value) => { +const formatData = (format, value) => { if (value === undefined || (value === null && format !== "progress")) { return translate("prop.na"); } + switch (format) { case "int": return parseInt(value); @@ -208,60 +196,22 @@ const slaFormatData = (format, value) => { return numberFormat(value); case "layer": return numberFormat(value, false) + " mm"; - case "temp": - return numberFormat(value) + " °C"; - case "fan": - return numberFormat(value) + ` ${str_rpm}`; - case "resin": - return numberFormat(value) + ` ${str_ml}`; - case "cover": - return value - ? translate("prop.cover-closed") - : translate("prop.cover-opened"); - case "date": - return dateFormat(value); - case "progress": - return numberFormat((value || 0) * 100, true, 0) + "%"; - case "timeEst": - return formatEstimatedTime(value); - case "time": - return formatTime(value); - case "est-time": - return "~ " + formatTime(value); - case "expo": - return formatExposure(value); case "totalLayer": return totalLayers(value); case "material": return value || translate("prop.na"); - case "size": - return formatSize(value); - case "boolean": - return formatBoolean(value); - default: - return value; - } -}; - -/** - * Format the value data with format specificated for fdm type. - * @param {string} format - one of ["number", "temp", "fan", "pos", "date", "progress", "timeEst", "time"] - * @param {any} value - */ -const fdmFormatData = (format, value) => { - if (value === undefined || (value === null && format !== "progress")) { - return translate("prop.na"); - } - - switch (format) { - case "number": - return numberFormat(value); case "temp": return numberFormat(value) + " °C"; case "temp_int": return numberFormat(value, 0) + "°C"; case "fan": return numberFormat(value) + ` ${str_rpm}`; + case "resin": + return numberFormat(value) + ` ${str_ml}`; + case "cover": + return value + ? translate("prop.cover-closed") + : translate("prop.cover-opened"); case "print": return numberFormat(value || 0, true, 0) + "%"; case "pos": diff --git a/src/printer/components/errors.js b/src/printer/components/errors.js index 94e3a128..a6d8517d 100644 --- a/src/printer/components/errors.js +++ b/src/printer/components/errors.js @@ -22,12 +22,12 @@ export function handleError(result, options) { let message = result?.data?.message || options?.fallbackMessage?.message || "Action can not be performed"; - let isWarning = false; + let isWarning = options?.isWarning ?? false; if (result?.data) { const data = result.data; if (data.code) { - title += `- ${data.code}`; + title += ` - ${data.code}`; if (`${data.code}`[3] == "7") isWarning = true; } diff --git a/src/printer/components/files.js b/src/printer/components/files.js index 50ee7fbf..45ed56b6 100644 --- a/src/printer/components/files.js +++ b/src/printer/components/files.js @@ -69,7 +69,7 @@ function getCurrentApiPath(fileName) { return getApiPath(storage.path, path, fileName); } -function getApiPath(origin, path, file) { +export function getApiPath(origin, path, file) { const apiPath = ["/api/v1/files", origin, path, file].filter((e) => !!e) .join("/"); @@ -202,6 +202,9 @@ const updateFiles = (opts = {}) => { headers: { "If-None-Match": lastETag }, }) .then((result) => { + if (result.code === 304) { + return + } if (url !== getCurrentApiPath()) { // user navigated to other folder return; diff --git a/src/printer/components/job.js b/src/printer/components/job.js index 9d0e622d..7c873ecc 100644 --- a/src/printer/components/job.js +++ b/src/printer/components/job.js @@ -2,27 +2,35 @@ // Copyright (C) 2021 Prusa Research a.s. - www.prusa3d.com // SPDX-License-Identifier: GPL-3.0-or-later -import joinPaths from "../../helpers/join_paths"; import updateProperties from "./updateProperties"; -import { cancelJob, cancelPreview, pauseJob, resumeJob, startJob } from "./jobActions"; +import { + cancelJob, + cancelPreview, + pauseJob, + resumeJob, + startJob, +} from "./jobActions"; import { deleteFile, downloadFile } from "./fileActions"; -import { getImage, getJson } from "../../auth"; -import { handleError } from "./errors"; -import { renderProgressImg, updateProgressImg } from "./progressImage"; -import { setEnabled, setHidden, setVisible, showLoading, hideLoading } from "../../helpers/element"; +import { renderProgressImg } from "./progressImage"; +import { + setEnabled, + setHidden, + setVisible, + showLoading, + hideLoading, +} from "../../helpers/element"; import { updateProgressBar } from "./progressBar"; import { translate } from "../../locale_provider"; -import changeExposureTimesQuestion from "../sla/exposure"; -import { resinRefill } from "../sla/refill"; +import changeExposureTimesQuestion from "../views/exposure"; +import { resinRefill } from "../views/refill"; import { JobPendingStates, LinkState, OperationalStates } from "../../state"; import { setButtonLoading, unsetButtonLoading } from "../../helpers/button"; import printer from ".."; -import { context } from "../fdm"; +import { getApiPath } from "./files"; let pendingCommand = null; let pendingDownload = null; - /** * Rerender component without api calls. * @param {object} context Printer context. @@ -35,8 +43,7 @@ export function render(context) { } const visible = setComponentVisibility(context); - if (visible) - updateComponent(context); + if (visible) updateComponent(context); } /** @@ -48,21 +55,18 @@ export function update(context, isFilePreview = false) { if (pendingCommand && pendingCommand.state !== context.state) { pendingCommand = null; - } + } if (visible) { updateComponent(context, isFilePreview); } -}; +} function setComponentVisibility(context, isFilePreview) { const element = document.getElementById("job"); - if (!element) - return false; + if (!element) return false; - const visible = isFilePreview - ? !!context.files.selected - : !!context.job?.id; + const visible = isFilePreview ? !!context.files.selected : !!context.job?.id; setVisible(element, visible); return visible; @@ -100,16 +104,15 @@ function updateComponent(context, isFilePreview) { } function setupRefill(stateText) { - const preview = document.getElementById("preview-wrapper") - const refill = document.getElementById("refill-wrapper") - if (stateText == "Feed me" || stateText == "Pour in resin") { - if (stateText == "Pour in resin") { + const preview = document.getElementById("preview-wrapper"); + const refill = document.getElementById("refill-wrapper"); + if ([LinkState.REFILL, LinkState.POUR_IN_RESIN].includes(stateText)) { + if (stateText == LinkState.POUR_IN_RESIN) { translate("msg.sla-pour-resin", { query: "#sla-refill-text" }); } setHidden(preview); setVisible(refill); - } - else { + } else { setHidden(refill); setVisible(preview); } @@ -135,57 +138,82 @@ function setupProperties(isPreview) { function hideNaProperties(state, isFilePreview) { const naValue = translate("prop.na"); - document.getElementById("job").querySelectorAll(".job-details .job-prop").forEach(section => { - const group = section.querySelector(".job-prop-grid").children; - let hidden = true; - - for (const prop of group) { - var isNa = prop.querySelector("[data-type]")?.innerHTML.trim() === naValue; - setHidden(prop, isNa); - if (!isNa) - hidden = false; - } - - if (process.env.PRINTER_TYPE === "sla") - setHidden(document.getElementById("pnt-time-est"), true) + document + .getElementById("job") + .querySelectorAll(".job-details .job-prop") + .forEach((section) => { + const group = section.querySelector(".job-prop-grid").children; + let hidden = true; + + for (const prop of group) { + var isNa = + prop.querySelector("[data-type]")?.innerHTML.trim() === naValue; + setHidden(prop, isNa); + if (!isNa) hidden = false; + } - if (process.env.PRINTER_TYPE === "sla" - && section.id === "file-last-mod" - && !isFilePreview - && ["Busy", "Printing"].includes(state.text) - ) { - hidden = true; - } + if (process.env.PRINTER_TYPE === "sla") + setHidden(document.getElementById("pnt-time-est"), true); + if ( + process.env.PRINTER_TYPE === "sla" && + section.id === "file-last-mod" && + !isFilePreview && + ["Busy", "Printing"].includes(state.text) + ) { + hidden = true; + } - setHidden(section, hidden); - }) + setHidden(section, hidden); + }); } /* ===================================== SETUP BUTTONS ======================================= */ function setupButtons(state, dataSource, jobId) { const file = dataSource.file; - + setupCancelButton(state, jobId); setupStartButton(state, file, jobId); setupDeleteButton(state, file, jobId); setupDownloadButton(state, file, jobId); if (!!jobId) { - if (process.env.PRINTER_TYPE === "fdm") { + if (process.env.PRINTER_TYPE === "sla") { + setupExposureButton(state, dataSource, changeExposureTimesQuestion); + // pause for refill + setupPauseButton(state, jobId, "#job #refill"); + setupSlaResumeWithRefillButton(state, jobId); + setupSlaResumeNoRefillButton(state, jobId); + } else if (process.env.PRINTER_TYPE === "fdm") { setupPauseButton(state, jobId, "#job #pause"); setupResumeButton(state, jobId); } + } +} - if (process.env.PRINTER_TYPE === "sla") { - const jobFile = jobResult?.job?.file; - if (jobFile) - setupExposureButton(state, jobFile, changeExposureTimesQuestion); - setupPauseButton(state, jobId, "#job #refill"); - setupSlaResumeButton(state, "#job #continue"); - setupSlaResumeButton(state, "#job #back"); - } +function setupSlaResumeWithRefillButton(state, jobId) { + const btn = document.querySelector("#job #continue"); + setVisible(btn, [LinkState.REFILL, LinkState.POUR_IN_RESIN].includes(state)); + + if (btn) { + btn.onclick = () => + state === LinkState.REFILL ? resinRefill(jobId) : resumeJob(jobId); + } +} + +function setupSlaResumeNoRefillButton(state, jobId) { + const btn = document.querySelector("#job #back"); + const isVisible = LinkState.REFILL === state; + setVisible(btn, isVisible); + setEnabled(btn, true); + + if (btn) { + btn.onclick = () => { + setEnabled(btn, false); + pendingCommand = { code: "resume", state: state }; + resumeJob(jobId).catch(() => (pendingCommand = null)); + }; } } @@ -193,23 +221,27 @@ function setupCancelButton(state, jobId) { const btnStop = document.querySelector("#job #stop"); const btnClose = document.querySelector("#job-close"); const linkState = LinkState.fromApi(state); - const isJobPreview = OperationalStates.includes(linkState); - const enabled = !pendingCommand && JobPendingStates.includes(state) + const isJobPreview = [ + LinkState.IDLE, + LinkState.READY, + LinkState.FINISHED, + ].includes(linkState); + const enabled = !pendingCommand && JobPendingStates.includes(state); const context = printer.getContext(); - + setEnabled(btnStop, enabled); if (btnStop) { if (jobId) { - const isVisible = jobId || (process.env.PRINTER_TYPE === "sla" && state.text != "Feed me"); + const isVisible = jobId && ![LinkState.REFILL].includes(state); setVisible(btnStop, isVisible); btnStop.onclick = () => { cancelJob(jobId, { onConfirm: () => { - pendingCommand = {code: "stop", state: state}; + pendingCommand = { code: "stop", state: state }; setEnabled(btnStop, false); }, - onError: () => pendingCommand = null + onError: () => (pendingCommand = null), }); }; } @@ -217,9 +249,7 @@ function setupCancelButton(state, jobId) { if (btnClose) { setVisible(btnClose, isJobPreview || !jobId); - btnClose.onclick = !jobId - ? () => context.selectFile(null) - : cancelPreview; + btnClose.onclick = !jobId ? () => context.selectFile(null) : cancelPreview; } } @@ -228,7 +258,7 @@ function setupStartButton(state, file, jobId) { const actionAllowed = OperationalStates.includes(state); if (btn) { - setVisible(btn, actionAllowed) + setVisible(btn, actionAllowed); setEnabled(btn, actionAllowed); btn.onclick = () => startJob(state !== LinkState.READY, file.resource); } @@ -243,43 +273,24 @@ function setupPauseButton(state, jobId, selector) { if (btn) { btn.onclick = () => { setEnabled(btn, false); - pendingCommand = {code: "pause", state: state}; - pauseJob(jobId) - .catch(() => pendingCommand = null); + pendingCommand = { code: "pause", state: state }; + pauseJob(jobId).catch(() => (pendingCommand = null)); }; } } function setupResumeButton(state, jobId) { const btn = document.querySelector("#job #resume"); - const isPaused = state === LinkState.PAUSED; + const isPaused = [LinkState.PAUSED].includes(state); setVisible(btn, isPaused); setEnabled(btn, !pendingCommand && isPaused); if (btn) { btn.onclick = () => { setEnabled(btn, false); - pendingCommand = {code: "resume", state: state}; - resumeJob(jobId) - .catch(() => pendingCommand = null); - } - } -} - -function setupSlaResumeButton(state, selector) { - const btn = document.querySelector(selector); - if (selector.includes("#back")) - setVisible(btn, state.flags.paused && state.text === "Feed me"); - else - setVisible(btn, state.flags.paused); - - - if (btn) { - if (state.text == "Feed me" && !selector.includes("#back")) { - btn.onclick = resinRefill; - } else { - btn.onclick = resumeJob; - } + pendingCommand = { code: "resume", state: state }; + resumeJob(jobId).catch(() => (pendingCommand = null)); + }; } } @@ -292,20 +303,22 @@ function setupDeleteButton(state, file, jobId) { btn.onclick = () => { deleteFile(file.resource, fileDisplayName, () => { if (!jobId) { + const context = printer.getContext(); context.selectFile(null); } }); - } + }; } } function setupDownloadButton(state, file, jobId) { const btn = document.querySelector("#job #download"); if (btn) { - const isVisible = !jobId && file.refs?.download && ( - !pendingDownload || pendingDownload === file.refs.download - ); - + const isVisible = + !jobId && + file.refs?.download && + (!pendingDownload || pendingDownload === file.refs.download); + const fileDisplayName = file.display_name || file.name; setVisible(btn, isVisible); if (isVisible) { @@ -314,28 +327,24 @@ function setupDownloadButton(state, file, jobId) { setButtonLoading(btn); downloadFile(file.refs.download, fileDisplayName, () => { pendingDownload = null; - unsetButtonLoading(btn) + unsetButtonLoading(btn); }); - } + }; } } } -function setupExposureButton(state, jobFile, changeExposureTimesQuestion) { +function setupExposureButton(state, job, changeExposureTimesQuestion) { const btn = document.querySelector("#job #exposure"); if (btn) { - setVisible(btn, state.text === "Pour in resin" || state.text === "Printing"); - setEnabled(btn, state.flags.operational); - btn.onclick = () => changeExposureTimesQuestion(jobFile); - } -} - -function setupBackButton(state) { - const btn = document.querySelector("#job #back"); - - if (btn) { - setVisible(btn, state.flags.paused && state.text === "Feed me"); - btn.onclick = resumeJob; + const canSetupExposure = [ + ...OperationalStates, + LinkState.PRINTING, + LinkState.POUR_IN_RESIN, + ].includes(state); + setVisible(btn, canSetupExposure); + setEnabled(btn, canSetupExposure); + btn.onclick = () => changeExposureTimesQuestion(job); } } diff --git a/src/printer/components/question.js b/src/printer/components/question.js index cae3b3f2..b8390fd8 100644 --- a/src/printer/components/question.js +++ b/src/printer/components/question.js @@ -31,7 +31,7 @@ export const doQuestion = (data) => { no: (cb) => cb(), yesText: translate("btn.yes"), noText: translate("btn.no"), - next: "#files", + next: "#dashboard", }, data ); diff --git a/src/printer/components/settings.js b/src/printer/components/settings.js index 5476a288..4c2caec1 100644 --- a/src/printer/components/settings.js +++ b/src/printer/components/settings.js @@ -236,8 +236,12 @@ function initSettings() { getJson("/api/settings") .then((result) => { const settings = result.data; - initPrinterSettings(settings); - initUserSettings(settings); + if (process.env["WITH_PRINTER_SETTINGS"]) { + initPrinterSettings(settings); + } + if (process.env["WITH_USER_SETTINGS"]) { + initUserSettings(settings); + } }) .catch((result) => handleError(result)); } diff --git a/src/printer/components/storage.js b/src/printer/components/storage.js index 462439d3..05e54ad8 100644 --- a/src/printer/components/storage.js +++ b/src/printer/components/storage.js @@ -34,7 +34,7 @@ const update = (storages, selectedStorage, onSelect, updateDetails = false) => { const storage = storages[storageType]; const label = tab.querySelector("p"); - isVisible = true; + isVisible = storages[storageType].available; if (storage.name) { label.innerText = storage.name; } diff --git a/src/printer/components/toast.js b/src/printer/components/toast.js index 20792d0d..8674740d 100644 --- a/src/printer/components/toast.js +++ b/src/printer/components/toast.js @@ -38,7 +38,9 @@ export const createToast = (title, message, type) => { function show({ title, message, type, onClose }) { const article = createToast(title, message, type); const close = () => { - toast_context.removeChild(article); + if (toast_context.contains(article)) { + toast_context.removeChild(article); + } onClose?.(); }; diff --git a/src/printer/fdm/context.js b/src/printer/context.js similarity index 57% rename from src/printer/fdm/context.js rename to src/printer/context.js index 491e43d8..11653e70 100644 --- a/src/printer/fdm/context.js +++ b/src/printer/context.js @@ -1,7 +1,8 @@ -import { getImage, getJson } from "../../auth"; -import { handleError } from "../components/errors"; -import { LinkState } from "../../state"; -import { getEstimatedEnd } from "../common"; +import { getImage, getJson } from "../auth"; +import { handleError } from "./components/errors"; +import { LinkState } from "../state"; +import { getEstimatedEnd } from "./common"; +import { translate } from "../locale_provider"; export class Context { constructor() { @@ -35,18 +36,18 @@ export class Context { ok: true, message: "OK", settings: { - hostname: 'connect.prusa3d.com', + hostname: "connect.prusa3d.com", tls: true, - port: 0 - } + port: 0, + }, }, printer: { ok: true, message: "OK", settings: { - port: '', + port: "", baudrate: 115200, - } + }, }, }; this.files = { @@ -61,19 +62,18 @@ export class Context { } updateConnection() { - return getJson("/api/connection", { method: "GET" }) - .then(res => { - this.link.connect.settings = { - hostname: res.data.connect.hostname, - port: res.data.connect.port, - tls: res.data.connect.tls, - }; - this.link.connect.registration = res.data.connect.registration; - this.link.printer.settings = { - port: res.data.current.port, - baudrate: res.data.current.baudrate, - }; - }); + return getJson("/api/connection", { method: "GET" }).then((res) => { + this.link.connect.settings = { + hostname: res.data.connect?.hostname, + port: res.data.connect?.port, + tls: res.data.connect?.tls, + }; + this.link.connect.registration = res.data.connect?.registration; + this.link.printer.settings = { + port: res.data.current?.port, + baudrate: res.data.current?.baudrate, + }; + }); } update({ status, printer }) { @@ -105,12 +105,28 @@ export class Context { hostname: printer.hostname, port: printer.port, }; + this.fileExtensions = + printer.project_extensions ?? process.env["FILE_EXTENSIONS"].split(","); } updateTelemetry(printer) { this.state = LinkState.fromApi(printer.state.toUpperCase()); + if (process.env["PRINTER_TYPE"] === "sla") { + const isCalibrated = this.telemetry.isCalibrated ?? true; + if (!printer.is_calibrated && isCalibrated) { + handleError({ + data: { + code: 10113, + title: translate("ntf.calibration-error"), + message: translate("ntf.n-calibrated"), + url: "https://help.prusa3d.com/en/10113", + } + }); + } + } this.telemetry = { temperature: { + // fdm nozzle: { current: printer.temp_nozzle, target: printer.target_nozzle, @@ -119,6 +135,16 @@ export class Context { current: printer.temp_bed, target: printer.target_bed, }, + // sla + ambient: { + current: printer.temp_ambient, + }, + cpu: { + current: printer.temp_cpu, + }, + uvLED: { + current: printer.temp_uv_led, + }, }, axis: { x: printer.axis_x, @@ -128,15 +154,22 @@ export class Context { flow: printer.flow, speed: printer.speed, fan: { + // fdm hotend: printer.fan_hotend, print: printer.fan_print, + // sla + blower: printer.fan_blower, + rear: printer.fan_rear, + uvLED: printer.fan_uv_led, }, + coverClosed: printer.cover_closed, + isCalibrated: printer.is_calibrated, }; // hide status if connect is not supported this.link.connect.message = printer.status_connect?.message ?? ""; this.link.connect.ok = printer.status_connect?.ok; // just suppress the status if unsupported by the printer - this.link.printer.message = printer.status_printer?.message ?? 'ok'; + this.link.printer.message = printer.status_printer?.message ?? "ok"; this.link.printer.ok = printer.status_printer?.ok ?? true; } @@ -144,50 +177,37 @@ export class Context { const oldJobId = this.job?.id || null; const newJobId = job?.id || null; - if (oldJobId !== newJobId) { + if (oldJobId !== newJobId || this.job?.dirty) { if (!newJobId) { this.job = undefined; return; } - getJson("/api/v1/job") - .then((response) => { - const data = response.data; - if (data.id !== this.job.id) { - return; - } - - this.job = { - ...this.job, - file: mapFile(data.file), - thumbnail: { - source: !data.file.refs?.thumbnail, - ready: !data.file.refs?.thumbnail, - url: undefined, - }, - }; - - if (!this.job.thumbnail.ready) { - getImage(this.job.file.refs.thumbnail) - .then(({ url }) => { - if (this.job.id === newJobId) { - this.job.thumbnail.url = url; - } - }) - .catch((e) => console.error("Failed to fetch thumbnail", e)) - .finally(() => (this.job.thumbnail.ready = true)); - } - }) - .catch((err) => handleError(err)); + this.updateJobDetails(); } - if (newJobId) { + if (job && newJobId) { const lastTimeRemaining = this.job?.timeRemaining; - const thisTimeRemaning = job.time_remaining; + const thisTimeRemaining = job.time_remaining; const estimatedEnd = - lastTimeRemaining != thisTimeRemaning - ? getEstimatedEnd(thisTimeRemaning) + lastTimeRemaining != thisTimeRemaining + ? getEstimatedEnd(thisTimeRemaining) : this.job?.estimatedEnd; + if (process.env.PRINTER_TYPE === "sla") { + const isResinLow = !!this.job?.resinLow; + if (!isResinLow && job.resin_low) { + handleError({ + data: { + code: 10712, + title: translate("ntf.low-resin.title"), + message: translate("ntf.low-resin.message"), + url: "https://help.prusa3d.com/en/10712", + } + }, {isWarning: true}); + } + } + this.job = { + dirty: false, file: undefined, ...this.job, timePrinting: job.time_printing, @@ -195,10 +215,59 @@ export class Context { progress: job.progress, timeRemaining: job.time_remaining, estimatedEnd, + ...(process.env.PRINTER_TYPE === "sla" + ? { + exposureTime: job.exposure_time, + exposureTimeCalibration: job.exposure_time_calibration, + exposureTimeFirst: job.exposure_time_first, + exposureUserProfile: job.exposure_user_profile, + currentLayer: job.current_layer, + resinRemaining: job.resin_remaining, + resinConsumed: job.resin_consumed, + resinLow: job.resin_low, + } + : {}), }; } } + updateJobDetails() { + return getJson("/api/v1/job") + .then((response) => { + const data = response.data; + if (data.id !== this.job.id) { + return; + } + + this.job = { + ...this.job, + dirty: false, + file: mapFile(data.file), + thumbnail: { + source: !data.file.refs?.thumbnail, + ready: !data.file.refs?.thumbnail, + url: undefined, + }, + }; + + if (!this.job.thumbnail.ready) { + const jobId = this.job.id; + getImage(this.job.file.refs.thumbnail) + .then(({ url }) => { + if (this.job.id === jobId) { + this.job.thumbnail.url = url; + } + }) + .catch((e) => console.error("Failed to fetch thumbnail", e)) + .finally(() => (this.job.thumbnail.ready = true)); + } + }) + .catch((err) => { + this.job.dirty = true; + handleError(err); + }); + } + updateStorage(storage) { Object.keys(storage).forEach((entry) => { const data = { @@ -250,14 +319,14 @@ export class Context { const now = Math.round(Date.now() / 1000); const fileSize = this.transfer?.file?.size || 0; const timeTransferring = transfer.time_transferring; - const timeStarted = timeTransferring !== undefined - ? now - timeTransferring - : undefined; + const timeStarted = + timeTransferring !== undefined ? now - timeTransferring : undefined; const dataTransferred = transfer.data_transferred; const dataRemaining = fileSize - dataTransferred; - const timeRemaining = (timeTransferring > 0 && dataRemaining >= 0) - ? (dataRemaining / (dataTransferred / timeTransferring)) - : undefined; + const timeRemaining = + timeTransferring > 0 && dataRemaining >= 0 + ? dataRemaining / (dataTransferred / timeTransferring) + : undefined; this.transfer = { ...this.transfer, @@ -266,7 +335,7 @@ export class Context { timeRemaining, id: newId, progress: transfer.progress, - dataTransferred + dataTransferred, }; } } @@ -282,7 +351,7 @@ export class Context { this.files.selected = null; return; } - + const thumbnailSource = file.refs?.thumbnail; this.files.selected = { @@ -297,16 +366,16 @@ export class Context { if (!file.meta) { const resource = this.files.selected.file.resource; - getJson(resource) - .then(response => { - const fullFileInfo = mapFile({...response.data, resource}); - if (this.files.selected.file.resource === resource) { - this.files.selected.file.meta = fullFileInfo.meta; - this.files.selected.timeRemaining = fullFileInfo.meta?.estimatedPrintTime; - } - }); + getJson(resource).then((response) => { + const fullFileInfo = mapFile({ ...response.data, resource }); + if (this.files.selected.file.resource === resource) { + this.files.selected.file.meta = fullFileInfo.meta; + this.files.selected.timeRemaining = + fullFileInfo.meta?.estimatedPrintTime; + } + }); } - + if (thumbnailSource) { getImage(thumbnailSource) .then(({ url }) => { @@ -324,8 +393,8 @@ export class Context { } } -const getFileResource = (path, name) => - `${path}${path.endsWith("/") ? "" : "/"}${name}`; +export const getFileResource = (path, name) => + `/api/v1/files${path}${path.endsWith("/") ? "" : "/"}${name}`; const mapFile = (data) => ({ resource: data.resource ?? getFileResource(data.path, data.name), @@ -344,6 +413,10 @@ const mapFile = (data) => ({ filamentType: data.meta?.filament_type, layerHeight: data.meta?.layer_height, estimatedPrintTime: data.meta?.estimated_print_time, + exposureTime: data.meta?.exposure_time, + exposureTimeCalibration: data.meta?.exposure_time_calibration, + exposureTimeFirst: data.meta?.exposure_time_first, + exposureUserProfile: data.meta?.exposure_user_profile, }, - readOnly: data.read_only || data.ro + readOnly: data.read_only || data.ro, }); diff --git a/src/printer/fdm/index.js b/src/printer/fdm/index.js deleted file mode 100644 index 67db4ed7..00000000 --- a/src/printer/fdm/index.js +++ /dev/null @@ -1,151 +0,0 @@ -// This file is part of the Prusa Link Web -// Copyright (C) 2021 Prusa Research a.s. - www.prusa3d.com -// SPDX-License-Identifier: GPL-3.0-or-later - -import * as graph from "../components/temperature_graph"; -import dashboard from "./dashboard.js"; -import files from "../components/files"; -import question from "../components/question.js"; -import { buildTitle, getPrinterLabel, getStatusForTitle } from "../common.js"; -import { updateProperties } from "../components/updateProperties.js"; -import { translate } from "../../locale_provider"; -import { LinkState, translateState } from "../../state"; -import updateConnectionStatus from "../components/updateConnectionStatus"; -import { currentRoute } from "../../router"; -import { Context } from "./context"; - -export const context = new Context(); - -const updatePrinterTitle = (obj) => { - const newTitle = () => { - const label = document.getElementById("title-printer"); - if (label) { - label.innerHTML = getPrinterName(); - } - }; - const load = obj.load; - obj.load = () => { - newTitle(); - load(context); - }; - return obj; -}; - -const getPrinterName = () => getPrinterLabel(context); - -const updatePrinterStatus = (state) => { - const linkState = state; - const elem = document.getElementById("printer-status"); - if (elem) { - elem.innerHTML = translateState(linkState); - } -}; - -const buildRouteTitle = (titleItems) => buildTitle([ - ...titleItems, - getPrinterName(), - process.env["APP_NAME"] -]); - -let currentModule = dashboard; -const fdm = { - routes: [ - { - path: "dashboard", - html: require("../../views/dashboard.html"), - module: updatePrinterTitle(dashboard), - getTitle: () => translate("home.link"), - }, - { - path: "question", - html: require("../../views/question.html"), - module: updatePrinterTitle(question), - }, - process.env.WITH_FILES ? - { - path: "files", - html: require("../../views/files.html"), - module: updatePrinterTitle(files), - getTitle: () => translate("proj.storage"), - } - : null, - process.env.WITH_SETTINGS ? - { - path: "settings", - html: require("../../views/settings.html"), - module: updatePrinterTitle(require("../components/settings.js").default), - getTitle: () => translate("settings.title"), - } - : null, - process.env.WITH_CONTROLS ? - { - path: "control", - html: require("../../views/control.html"), - module: updatePrinterTitle(require("../components/control.js").default), - getTitle: () => translate("control.link"), - } : null, - process.env.WITH_CAMERAS ? - { - path: "cameras", - html: require("../../views/cameras.html"), - module: updatePrinterTitle(require("../components/cameras.js").default), - getTitle: () => translate("cameras.link"), - } : null, - ].filter(route => route != null), - init: (apiResult) => { - context.update(apiResult); - initTemperatureGraph(); - }, - update: (apiResult) => { - context.update(apiResult); - - const page = currentRoute(); - const stateText = getStatusForTitle(context); - document.title = buildRouteTitle([ - stateText, - fdm.routes.find(route => route.path === page).getTitle() - ]); - - updateProperties("telemetry", context); - updatePrinterStatus(context.state); - updateTemperatureGraph(context.telemetry); - updateModule(); - }, - setConnected: (isConnected) => { - updateConnectionStatus({ - link: context.link, - isConnected, - }); - }, - setModule: (module) => { - currentModule = module; - }, - getContext: () => { - return context; - }, -}; - -const initTemperatureGraph = () => { - const maxTemp = 300; - - let map = new Map([ - ["temp-line-blue", []], - ["temp-line-orange", []], - ]); - - graph.init(map, maxTemp); - graph.render(); -}; - -const updateTemperatureGraph = (telemetry) => { - const now = new Date().getTime(); - graph.update("temp-line-blue", [now, telemetry.temperature.bed.current]); - graph.update("temp-line-orange", [now, telemetry.temperature.nozzle.current]); - graph.render(); -}; - -const updateModule = () => { - if (currentModule && currentModule.update) currentModule.update(context); -}; - -export default fdm; diff --git a/src/printer/index.js b/src/printer/index.js index 7464d4c3..b800666c 100644 --- a/src/printer/index.js +++ b/src/printer/index.js @@ -2,9 +2,174 @@ // Copyright (C) 2021 Prusa Research a.s. - www.prusa3d.com // SPDX-License-Identifier: GPL-3.0-or-later -const printer = (() => { - if (process.env.PRINTER_TYPE === "sla") return require("./sla"); - if (process.env.PRINTER_TYPE === "fdm") return require("./fdm"); -})().default; +import * as graph from "./components/temperature_graph"; +import dashboard from "./views/dashboard.js"; +import files from "./components/files"; +import question from "./components/question.js"; +import refill from "./views/refill.js"; +import { buildTitle, getPrinterLabel, getStatusForTitle } from "./common.js"; +import { updateProperties } from "./components/updateProperties.js"; +import { translate } from "../locale_provider"; +import { translateState } from "../state"; +import updateConnectionStatus from "./components/updateConnectionStatus"; +import { currentRoute } from "../router"; +import { Context } from "./context"; + +const context = new Context(); + +const updatePrinterTitle = (obj) => { + const newTitle = () => { + const label = document.getElementById("title-printer"); + if (label) { + label.innerHTML = getPrinterName(); + } + }; + const load = obj.load; + obj.load = () => { + newTitle(); + load(context); + }; + return obj; +}; + +const getPrinterName = () => getPrinterLabel(context); + +const updatePrinterStatus = (state) => { + const linkState = state; + const elem = document.getElementById("printer-status"); + if (elem) { + elem.innerHTML = translateState(linkState); + } +}; + +const buildRouteTitle = (titleItems) => + buildTitle([...titleItems, getPrinterName(), process.env["APP_NAME"]]); + +let currentModule = dashboard; +const printer = { + routes: [ + { + path: "dashboard", + html: require("../views/dashboard.html"), + module: updatePrinterTitle(dashboard), + getTitle: () => translate("home.link"), + }, + { + path: "question", + html: require("../views/question.html"), + module: updatePrinterTitle(question), + }, + process.env.PRINTER_TYPE === "sla" + ? { + path: "refill", + html: require("../views/refill.html"), + module: updatePrinterTitle(refill), + } + : null, + process.env.WITH_FILES + ? { + path: "files", + html: require("../views/files.html"), + module: updatePrinterTitle(files), + getTitle: () => translate("proj.storage"), + } + : null, + process.env.WITH_SETTINGS + ? { + path: "settings", + html: require("../views/settings.html"), + module: updatePrinterTitle( + require("./components/settings.js").default + ), + getTitle: () => translate("settings.title"), + } + : null, + process.env.WITH_CONTROLS + ? { + path: "control", + html: require("../views/control.html"), + module: updatePrinterTitle( + require("./components/control.js").default + ), + getTitle: () => translate("control.link"), + } + : null, + process.env.WITH_CAMERAS + ? { + path: "cameras", + html: require("../views/cameras.html"), + module: updatePrinterTitle( + require("./components/cameras.js").default + ), + getTitle: () => translate("cameras.link"), + } + : null, + ].filter((route) => route != null), + init: (apiResult) => { + context.update(apiResult); + initTemperatureGraph(); + }, + update: (apiResult) => { + context.update(apiResult); + + const page = currentRoute(); + const stateText = getStatusForTitle(context); + document.title = buildRouteTitle([ + stateText, + printer.routes.find((route) => route.path === page).getTitle(), + ]); + + updateProperties("telemetry", context); + updatePrinterStatus(context.state); + updateTemperatureGraph(context.telemetry); + updateModule(); + }, + setConnected: (isConnected) => { + updateConnectionStatus({ + link: context.link, + isConnected, + }); + }, + setModule: (module) => { + currentModule = module; + }, + getContext: () => { + return context; + }, +}; + +const initTemperatureGraph = () => { + let maxTemp = (process.env.PRINTER_TYPE === "sla") ? 100 : 300; + + const map = new Map([ + ["temp-line-blue", []], + ["temp-line-orange", []], + ]); + + if (process.env.PRINTER_TYPE === "sla") { + map.set("temp-line-yellow", []) + } + + graph.init(map, maxTemp); + graph.render(); +}; + +const updateTemperatureGraph = (telemetry) => { + const now = new Date().getTime(); + if (process.env.PRINTER_TYPE === "fdm") { + graph.update("temp-line-blue", [now, telemetry.temperature.bed.current]); + graph.update("temp-line-orange", [now, telemetry.temperature.nozzle.current]); + } + if (process.env.PRINTER_TYPE === "sla") { + graph.update("temp-line-blue", [now, telemetry.temperature.ambient.current]); + graph.update("temp-line-orange", [now, telemetry.temperature.uvLED.current]); + graph.update("temp-line-yellow", [now, telemetry.temperature.cpu.current]); + } + graph.render(); +}; + +const updateModule = () => { + if (currentModule && currentModule.update) currentModule.update(context); +}; export default printer; diff --git a/src/printer/sla/dashboard.js b/src/printer/sla/dashboard.js deleted file mode 100644 index b4cc7f91..00000000 --- a/src/printer/sla/dashboard.js +++ /dev/null @@ -1,26 +0,0 @@ -// This file is part of the Prusa Link Web -// Copyright (C) 2021 Prusa Research a.s. - www.prusa3d.com -// SPDX-License-Identifier: GPL-3.0-or-later - -import * as graph from "../components/temperature_graph"; -import upload from "../components/upload"; -import { translate } from "../../locale_provider"; -import * as job from "../components/job"; -import { LinkState } from "../../state"; -import sla from "."; -import { getStatusForTitle } from "../common"; - -const load = (context) => { - translate("home.link", { query: "#title-status-label" }); - upload.init("local", "", context.fileExtensions); - graph.render(); - update(context); -}; - -const update = (context) => { - const linkState = context.state; - job.update(context); - upload.update(linkState); -}; - -export default { load, update }; diff --git a/src/printer/sla/index.js b/src/printer/sla/index.js deleted file mode 100644 index 73fbf861..00000000 --- a/src/printer/sla/index.js +++ /dev/null @@ -1,219 +0,0 @@ -// This file is part of the Prusa Link Web -// Copyright (C) 2021 Prusa Research a.s. - www.prusa3d.com -// SPDX-License-Identifier: GPL-3.0-or-later - -import * as graph from "../components/temperature_graph"; -import dashboard from "./dashboard.js"; -import files from "../components/files"; -import question from "../components/question.js"; -import refill from "./refill.js"; -import { updateProperties } from "../components/updateProperties.js"; -import { translate } from "../../locale_provider"; -import { showLoading, hideLoading } from "../../helpers/element"; -import { getPrinterLabel, getStatusForTitle } from "../common.js"; -import updateConnectionStatus from "../components/updateConnectionStatus"; - -import { currentRoute } from "../../router"; - -const context = { - /** Result of `api/v1/status */ - status: undefined, - - /** Result of `api/version`. */ - version: undefined, - /** Result of `api/printer`. */ - printer: undefined, - /** Result of `api/job`.job */ - current: undefined, - /** Result of `api/connection`. */ - connection: undefined, - /** Supported file extensions. */ - fileExtensions: [], -}; - -const updatePrinterTitle = (obj) => { - const newHostname = () => { - const hostnameLabel = document.getElementById("title-printer"); - if (hostnameLabel) { - hostnameLabel.innerHTML = getPrinterName(); - } - }; - const load = obj.load; - obj.load = () => { - newHostname(); - load(context); - }; - return obj; -}; - -const getPrinterName = () => getPrinterLabel(context); - - -const updatePrinterStatus = (state) => { - if (state) { - const query = { query: "#printer-status" }; - if (state.flags.error || state.flags.closedOrError) - translate("ntf.error", query); - else if (!state.flags.operational) - translate("prop.st-busy", query); - else if (state.flags.paused) { - if (state.text === "Pour in resin") - translate("prop.st-pour-resin", query); - else if (state.text === "Feed me") - translate("prop.st-feedme", query); - else - translate("prop.st-paused", query); - } - else if (state.flags.printing) - translate("prop.st-printing", query); - else - translate("prop.st-idle", query); - } -}; - -const buildRouteTitle = (titleItems) => buildTitle([ - ...titleItems, - getPrinterName(), - process.env["APP_NAME"] -]); - -let currentModule = dashboard; -const sla = { - routes: [ - { - path: "dashboard", - html: require("../../views/dashboard.html"), - module: updatePrinterTitle(dashboard), - getTitle: () => translate("home.link"), - }, - { - path: "question", - html: require("../../views/question.html"), - module: updatePrinterTitle(question), - }, - { - path: "refill", - html: require("../../views/refill.html"), - module: updatePrinterTitle(refill), - }, - process.env.WITH_FILES ? - { - path: "files", - html: require("../../views/files.html"), - module: updatePrinterTitle(files), - getTitle: () => translate("proj.storage"), - } - : null, - process.env.WITH_SETTINGS ? - { - path: "settings", - html: require("../../views/settings.html"), - module: updatePrinterTitle(require("../components/settings.js").default), - getTitle: () => translate("settings.title"), - } - : null, - process.env.WITH_CONTROLS ? - { - path: "control", - html: require("../../views/control.html"), - module: updatePrinterTitle(require("../components/control.js").default), - getTitle: () => translate("control.link"), - } : null, - ].filter(route => route != null), - init: (apiResult) => { - updateContext(apiResult); - const exts = apiResult.profiles?.payload?.data?.profiles[0]?.projectExtensions; - context.fileExtensions = exts || process.env.FILE_EXTENSIONS; - initTemperatureGraph(); - }, - update: (apiResult) => { - const page = currentRoute(); - const stateText = getStatusForTitle(context); - document.title = buildRouteTitle([ - stateText, - fdm.routes.find(route => route.path === page).getTitle() - ]); - - updateContext(apiResult); - if (context.printer.state.flags.operational) - hideLoading(); - else - showLoading(); - updateProperties("telemetry", context.printer); - updatePrinterStatus(context.printer.state); - updateTemperatureGraph(context.printer); - updateModule(); - }, - setConnected: (isConnected) => { - updateConnectionStatus({ - connection: context.connection, - isConnected, - }); - }, - setModule: (module) => { - currentModule = module; - }, - getContext: () => { - return context; - }, -}; - -const updateContext = ({ status, connection, job, printer, version }) => { - if (status?.ok && status.payload) { - context.status = status.payload.data; - } - - /* - if (connection?.ok && connection.payload) { - context.connection = connection.payload.data; - } - if (job?.ok && job.payload) { - context.current = job.payload.data; - } - if (printer?.ok && printer.payload) { - context.printer = printer.payload.data; - } - */ - - if (version?.ok && version.payload) { - context.version = version.payload; - } -} - -const initTemperatureGraph = () => { - const maxTemp = 100; - - let map = new Map([ - ["temp-line-blue", []], - ["temp-line-orange", []], - ["temp-line-yellow", []], - ]); - - graph.init(map, maxTemp); - graph.render(); -}; - -const updateTemperatureGraph = (data) => { - const now = new Date().getTime(); - graph.update("temp-line-blue", [ - now, - data.temperature.chamber.actual, // Original Prusa SL1 uses Chamber for ambient temp - ]); - graph.update("temp-line-orange", [ - now, - data.temperature.tool0 - .actual /* TODO: API collision - Original Prusa SL1 uses - Extruderfor UV LED temp - current API provides only tool0 */, - ]); - graph.update("temp-line-yellow", [ - now, - data.temperature.bed.actual, // Original Prusa SL1 uses Bed for CPU temperature - ]); - graph.render(); -}; - -const updateModule = () => { - if (currentModule && currentModule.update) currentModule.update(context); -}; - -export default sla; diff --git a/src/printer/sla/temperature.js b/src/printer/sla/temperature.js deleted file mode 100644 index fcc8620c..00000000 --- a/src/printer/sla/temperature.js +++ /dev/null @@ -1,13 +0,0 @@ -// This file is part of the Prusa Link Web -// Copyright (C) 2021 Prusa Research a.s. - www.prusa3d.com -// SPDX-License-Identifier: GPL-3.0-or-later - -import * as graph from "../components/temperature_graph"; -import { translate } from "../../locale_provider"; - -const load = () => { - translate("temps.title", { query: "#title-status-label" }); - graph.render(); -}; - -export default { load }; diff --git a/src/printer/fdm/dashboard.js b/src/printer/views/dashboard.js similarity index 92% rename from src/printer/fdm/dashboard.js rename to src/printer/views/dashboard.js index babb5add..ede75f67 100644 --- a/src/printer/fdm/dashboard.js +++ b/src/printer/views/dashboard.js @@ -7,10 +7,7 @@ import upload from "../components/upload"; import cameras from "../components/cameras"; import { translate } from "../../locale_provider"; import * as job from "../components/job"; -import { LinkState } from "../../state"; import { getJson } from "../../auth"; -import fdm from "."; -import { getStatusForTitle } from "../common"; const load = (context) => { translate("home.link", { query: "#title-status-label" }); diff --git a/src/printer/sla/exposure.js b/src/printer/views/exposure.js similarity index 91% rename from src/printer/sla/exposure.js rename to src/printer/views/exposure.js index 86857368..7a447763 100644 --- a/src/printer/sla/exposure.js +++ b/src/printer/views/exposure.js @@ -6,6 +6,7 @@ import { getJson } from "../../auth"; import { handleError } from "../components/errors"; import { doQuestion } from "../components/question"; import { translate } from "../../locale_provider"; +import printer from ".."; // import { setBusy } from "../components/busy"; const repeatInterval = 250; // milliseconds, how often should the value be updated when holding the button @@ -78,19 +79,20 @@ const setValue = (item_name, value, min, max, step) => { * @param {object} elements - {id: HTMLElement} * @param {HTMLDivElement} div html element to insert in modal */ -const setUpElements = (file, elements, div) => { +const setUpElements = (job, elements, div) => { const template = document.getElementById("exposure-item").content; + const file = job.file; div.className = "modal-exposure"; for (let expo in config) { - if (expo in file) { + if (expo in job && job[expo] !== undefined) { const elm = document.importNode(template, true); var minus = elm.getElementById("minus"); var plus = elm.getElementById("plus"); elm.getElementById("desc").innerHTML = config[expo].text; const value = elm.getElementById("value"); - value.dataset.value = file[expo].toFixed(0); + value.dataset.value = job[expo].toFixed(0); if (expo == "exposureUserProfile") { - switch(parseInt(file[expo])) { + switch(parseInt(job[expo])) { case 0: value.innerHTML = translate("exp-times.faster"); break; @@ -109,7 +111,7 @@ const setUpElements = (file, elements, div) => { plus = elm.getElementById("next"); minus.style.display = "block"; plus.style.display = "block"; - } else value.innerHTML = (file[expo] / 1000).toFixed(1); + } else value.innerHTML = (job[expo] / 1000).toFixed(1); const [min, max] = config[expo].limit; const setMinus = setValue(expo, value, min, max, -config[expo].step); minus.onclick = setMinus; @@ -141,12 +143,13 @@ const setUpElements = (file, elements, div) => { * Create a question for set up the exposure times * @param {object} file - job file information */ -const changeExposureTimesQuestion = (jobFile) => { +const changeExposureTimesQuestion = (job) => { + const context = printer.getContext(); const page = window.location.hash; history.pushState(null, document.title, page); const elements = {}; const div = document.createElement("div"); - setUpElements(jobFile, elements, div); + setUpElements(job, elements, div); doQuestion({ title: translate("btn.chg-print-set"), questionChildren: [div], @@ -164,8 +167,9 @@ const changeExposureTimesQuestion = (jobFile) => { }, body: JSON.stringify(result), }) + .then(() => context.updateJobDetails()) .catch((result) => handleError(result)) - .finally((result) => close()); + .finally(() => close()); }, yesText: translate("btn.save-chgs"), noText: translate("btn.cancel"), diff --git a/src/printer/sla/refill.js b/src/printer/views/refill.js similarity index 89% rename from src/printer/sla/refill.js rename to src/printer/views/refill.js index 6ce541a5..38af0c23 100644 --- a/src/printer/sla/refill.js +++ b/src/printer/views/refill.js @@ -6,14 +6,14 @@ import { getJson } from "../../auth"; import { handleError } from "../components/errors"; import { resumeJob } from "../components/jobActions"; -export const resinRefill = () => { +export const resinRefill = (jobId) => { getJson("/api/system/commands/custom/resinrefill", { method: "POST", headers: { "Content-Type": "application/json", } }).then(() => { - resumeJob(); + resumeJob(jobId); }).catch((result) => handleError(result)); }; diff --git a/src/printer/fdm/temperature.js b/src/printer/views/temperature.js similarity index 100% rename from src/printer/fdm/temperature.js rename to src/printer/views/temperature.js diff --git a/src/state.js b/src/state.js index a080293f..7219899d 100644 --- a/src/state.js +++ b/src/state.js @@ -6,14 +6,17 @@ export const LinkState = { IDLE: "IDLE", READY: "READY", BUSY: "BUSY", + POUR_IN_RESIN: "POUR IN RESIN", + REFILL: "FEED ME", PRINTING: "PRINTING", PAUSED: "PAUSED", FINISHED: "FINISHED", STOPPED: "STOPPED", + SELECTED: "SELECTED", ERROR: "ERROR", ATTENTION: "ATTENTION", fromApi: (linkState) => { - switch (linkState) { + switch (linkState.toUpperCase()) { case "IDLE": return LinkState.IDLE; case "READY": return LinkState.READY; case "BUSY": return LinkState.BUSY; @@ -23,6 +26,11 @@ export const LinkState = { case "STOPPED": return LinkState.STOPPED; case "ERROR": return LinkState.ERROR; case "ATTENTION": return LinkState.ATTENTION; + // sla specific + case "POUR IN RESIN": return LinkState.POUR_IN_RESIN; + case "FEED ME": return LinkState.REFILL; + case "SELECTED": return LinkState.SELECTED; + case "UNKNOWN": return LinkState.UNKNOWN; default: console.error(`Unsupported state: ${linkState}`); return LinkState.UNKNOWN; @@ -33,12 +41,15 @@ export const LinkState = { export const OperationalStates = [ LinkState.IDLE, LinkState.READY, - LinkState.FINISHED + LinkState.FINISHED, + LinkState.SELECTED, ]; export const JobPendingStates = [ LinkState.PRINTING, LinkState.PAUSED, + LinkState.POUR_IN_RESIN, + LinkState.SELECTED, ]; export const translateState = (state) => { @@ -52,6 +63,9 @@ export const translateState = (state) => { case LinkState.STOPPED: return translate("prop.st-stopped"); case LinkState.ERROR: return translate("prop.st-error"); case LinkState.ATTENTION: return translate("prop.st-attention"); + case LinkState.POUR_IN_RESIN: return translate("prop.st-pour-resin"); + case LinkState.SELECTED: return translate("prop.st-ready"); + case LinkState.REFILL: return translate("prop.st-feedme"); default: console.error(`Unsupported state: ${state}`); return translate("prop.st-unknown"); diff --git a/templates/components/job/props.html b/templates/components/job/props.html index 6b95a96b..1175718f 100644 --- a/templates/components/job/props.html +++ b/templates/components/job/props.html @@ -67,7 +67,7 @@ text: 'Current Layer', label: 'prop.current-layer', format: 'int', - location: 'progress.currentLayer', + location: 'currentLayer', type: 'job' } ] @@ -80,14 +80,14 @@ text: 'Remaining Resin', label: 'prop.sla-rmn-mt', format: 'resin', - location: 'resin.remaining', + location: 'resinRemaining', type: 'job' }, { text: 'Consumed Resin', label: 'prop.sla-csm-mt', format: 'resin', - location: 'resin.consumed', + location: 'resinConsumed', type: 'job' } ] diff --git a/templates/components/telemetry.html b/templates/components/telemetry.html index 422b8fc6..fd494d3a 100644 --- a/templates/components/telemetry.html +++ b/templates/components/telemetry.html @@ -9,12 +9,12 @@ {% if env.PRINTER_TYPE == "sla" %} {% set telemetry_list = [ - {'text': 'CPU temperature', label: 'prop.temp-cpu', 'icon': 'temperature_color.svg', 'format': 'temp', "locations": ["temperature.bed.actual"], enabled: true }, - {'text': 'UV LED temperature', label: 'prop.temp-led', 'icon': 'temperature_color.svg', 'format': 'temp', "locations": ["temperature.tool0.actual"], enabled: true }, - {'text': 'ambient temperature', label: 'prop.temp-amb', 'icon': 'temperature_color.svg', 'format': 'temp', "locations": ["temperature.chamber.actual"], enabled: true }, - {'text': 'UV LED fan', label: 'prop.fan-led', 'icon': 'fan_color.svg', 'format': 'fan', "location": "telemetry.fanUvLed", enabled: true }, - {'text': 'blower fan', label: 'prop.fan-blower', 'icon': 'fan_color.svg', 'format': 'fan', "location": "telemetry.fanBlower", enabled: true }, - {'text': 'rear fan', label: 'prop.fan-rear', 'icon': 'fan_color.svg', 'format': 'fan', "location": "telemetry.fanRear", enabled: true }, + {'text': 'CPU temperature', label: 'prop.temp-cpu', 'icon': 'temperature_color.svg', 'format': 'temp', "locations": ["telemetry.temperature.cpu.current"], enabled: true }, + {'text': 'UV LED temperature', label: 'prop.temp-led', 'icon': 'temperature_color.svg', 'format': 'temp', "locations": ["telemetry.temperature.uvLED.current"], enabled: true }, + {'text': 'ambient temperature', label: 'prop.temp-amb', 'icon': 'temperature_color.svg', 'format': 'temp', "locations": ["telemetry.temperature.ambient.current"], enabled: true }, + {'text': 'UV LED fan', label: 'prop.fan-led', 'icon': 'fan_color.svg', 'format': 'fan', "location": "telemetry.fan.uvLED", enabled: true }, + {'text': 'blower fan', label: 'prop.fan-blower', 'icon': 'fan_color.svg', 'format': 'fan', "location": "telemetry.fan.blower", enabled: true }, + {'text': 'rear fan', label: 'prop.fan-rear', 'icon': 'fan_color.svg', 'format': 'fan', "location": "telemetry.fan.rear", enabled: true }, {'text': 'cover state', label: 'prop.cover', 'icon': 'cover_color.svg', 'format': 'cover', "location": "telemetry.coverClosed", enabled: true } ] %} diff --git a/templates/pages/settings.html b/templates/pages/settings.html index e8d45353..5d9fd1ef 100644 --- a/templates/pages/settings.html +++ b/templates/pages/settings.html @@ -23,7 +23,7 @@ set section_version = [ {'text': 'api ', 'label': 'version.api', "location": "version.api", 'type': 'settings' }, {'text': 'hostname', 'label': 'version.hostname', "location": "version.hostname", 'type': 'settings' }, - {'text': 'server', 'label': 'version.server', "location": "version.server", 'type': 'settings' }, + {'text': 'firmware', 'label': 'version.firmware', "location": "version.firmware", 'type': 'settings' }, {'text': 'text', 'label': 'version.text', "location": "version.text", 'type': 'settings' }, {'text': 'sdk', 'label': 'version.sdk', "location": "version.sdk", 'type': 'settings' }, {'text': 'frontend', 'label': 'version.fe', 'value': env.APP_VERSION } @@ -109,14 +109,14 @@ {% set settings = [ {'text': 'version', 'label': 'version.title', 'settings': section_version }, - {'text': 'system version', 'label': 'sys-version.title', 'settings': [], 'id': 'sys-version' }, + {'text': 'system version', 'label': 'sys-version.title', 'settings': [], 'id': 'sys-version', 'condition': env.WITH_SYSTEM_VERSION }, {'text': 'updates', 'label': 'updates.title', 'settings': section_updates, 'condition': env.WITH_SYSTEM_UPDATES }, {'text': 'connection', 'label': 'conn.title', 'settings': section_connection, 'condition': env.WITH_CONNECTION }, - {'text': 'printer', 'label': 'printer.title', 'settings': section_printer }, - {'text': 'user', 'label': 'user.title', 'settings': section_user }, + {'text': 'printer', 'label': 'printer.title', 'settings': section_printer, 'condition': env.WITH_PRINTER_SETTINGS }, + {'text': 'user', 'label': 'user.title', 'settings': section_user, 'condition': env.WITH_USER_SETTINGS }, {'text': 'serial number', 'label': 'serial.label', 'settings': section_serial, 'condition': env.WITH_SERIAL }, {'text': 'api key', 'label': 'api_key.label', 'settings': section_api_key, 'condition': env.WITH_API_KEY_SETTING }, - {'text': 'logs', 'label': 'logs.title', 'settings': section_logs, 'condition': env.WITH_LOGS } + {'text': 'logs', 'label': 'logs.title', 'settings': section_logs, 'condition': env.WITH_LOGS, 'condition': env.WITH_LOGS } ] %} diff --git a/tools/preprocessing/extract-words.js b/tools/preprocessing/extract-words.js index fef80cb1..397252d2 100644 --- a/tools/preprocessing/extract-words.js +++ b/tools/preprocessing/extract-words.js @@ -111,7 +111,8 @@ const main = async (ci = false) => { if (!PRUSALATOR_UNSUPPORTED.includes(localeName)) { if (!ci && missingRemoteWords.length > 0) { try { - await apiClient.push(localeName, { data: JSON.stringify(missingRemoteWords) }); + const res = await apiClient.push(localeName, { data: JSON.stringify(missingRemoteWords) }); + console.log(`Push (${localeName}):`, res) } catch(e) { isSync = false; console.error(`Failed to push ${localeName} words:`, e?.response?.data?.message); diff --git a/webpack.config.js b/webpack.config.js index e7f9a899..1709486e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -64,6 +64,11 @@ module.exports = (env, args) => { WITH_API_KEY_SETTING: withDefault(env["WITH_API_KEY_SETTING"], false), WITH_NAME_SORTING_ONLY: withDefault(env["WITH_NAME_SORTING_ONLY"], false), WITH_SYSTEM_UPDATES: withDefault(env["WITH_SYSTEM_UPDATES"], false), + + WITH_SYSTEM_VERSION: withDefault(env["WITH_SYSTEM_VERSION"], false), + WITH_PRINTER_SETTINGS: withDefault(env["WITH_PRINTER_SETTINGS"], false), + WITH_USER_SETTINGS: withDefault(env["WITH_USER_SETTINGS"], false), + WITH_SERIAL: withDefault(env["WITH_SERIAL"], false), }; config["TPL_ASSETS_PATH"] = config["PRINTER_CODE"] == "m1" ? "../assets/m1" : "../assets";