diff --git a/config/server.js b/config/server.js index a91946e25..8aa6ec09e 100644 --- a/config/server.js +++ b/config/server.js @@ -39,6 +39,7 @@ const { configURI, getLastconnection, hasEnabledAuthentication, + marlinBftURI, } = require( path.normalize( __dirname + "/targets/" + target + "/" + subtarget + "/index.js" @@ -118,6 +119,12 @@ app.get("/command", function (req, res) { } }) +if (marlinBftURI) { + app.all("/printer-sd-transfer", function (req, res) { + marlinBftURI(req, res, SendWS) + }) +} + /*app.get("/sdfiles", function (req, res) { res.status(200); res.send( diff --git a/config/targets/Printer3D/Marlin/index.js b/config/targets/Printer3D/Marlin/index.js index a86a3a97e..af3e06f8e 100644 --- a/config/targets/Printer3D/Marlin/index.js +++ b/config/targets/Printer3D/Marlin/index.js @@ -30,6 +30,12 @@ let logindone = false const sessiontTime = 60000 const isTFT = false const roomTemperature = 20 +let binaryTransfer = { + status: "idle", + active: false, + progress: 0, + error: "", +} const temperatures = { T: [ { @@ -464,7 +470,7 @@ const commandsQuery = (req, res, SendWS, context) => { SendWS( "FIRMWARE_NAME:Marlin 2.0.9.1 (Sep 8 2021 17:07:06) SOURCE_CODE_URL:github.com/MarlinFirmware/Marlin PROTOCOL_VERSION:1.0 MACHINE_TYPE:MRR ESPA EXTRUDER_COUNT:1 UUID:cede2a2f-41a2-4748-9b12-c55c62f367ff\n" + "Cap:SERIAL_XON_XOFF:0\n" + - "Cap:BINARY_FILE_TRANSFER:0\n" + + "Cap:BINARY_FILE_TRANSFER:1\n" + "Cap:EEPROM:0\n" + "Cap:VOLUMETRIC:1\n" + "Cap:AUTOREPORT_POS:0\n" + @@ -1126,10 +1132,62 @@ const configURI = (req, res) => { ) } +const marlinBftURI = (req, res, SendWS) => { + const action = req.query.action || "status" + if (action == "start") { + if (binaryTransfer.active) { + res.status(409).json(binaryTransfer) + return + } + binaryTransfer = { + status: "checking_capabilities", + active: true, + progress: 0, + source: req.query.source || "", + destination: req.query.destination || "", + error: "", + startedAt: Date.now(), + } + SendWS("printerLink:captured:binary-transfer", false, false) + res.status(202).json(binaryTransfer) + return + } + if (action == "cancel") { + binaryTransfer.status = "cancelled" + binaryTransfer.active = false + SendWS("printerLink:released", false, false) + res.status(202).json(binaryTransfer) + return + } + if (action != "status") { + res.status(400).json({ + status: "failed", + active: false, + error: "Unknown action", + }) + return + } + if (binaryTransfer.active) { + binaryTransfer.progress = Math.min( + 100, + Math.floor((Date.now() - binaryTransfer.startedAt) / 50) + ) + binaryTransfer.status = + binaryTransfer.progress < 10 ? "checking_capabilities" : "writing" + if (binaryTransfer.progress == 100) { + binaryTransfer.status = "completed" + binaryTransfer.active = false + SendWS("printerLink:released", false, false) + } + } + res.json(binaryTransfer) +} + module.exports = { commandsQuery, configURI, loginURI, getLastconnection, hasEnabledAuthentication, + marlinBftURI, } diff --git a/dist/CNC/GRBL/index.html.br b/dist/CNC/GRBL/index.html.br index 817f804d4..67449e2f1 100644 Binary files a/dist/CNC/GRBL/index.html.br and b/dist/CNC/GRBL/index.html.br differ diff --git a/dist/CNC/GRBL/index.html.gz b/dist/CNC/GRBL/index.html.gz index 369dee19e..d89880059 100644 Binary files a/dist/CNC/GRBL/index.html.gz and b/dist/CNC/GRBL/index.html.gz differ diff --git a/dist/CNC/GRBLHal/index.html.br b/dist/CNC/GRBLHal/index.html.br index b85281363..e6ec5dd33 100644 Binary files a/dist/CNC/GRBLHal/index.html.br and b/dist/CNC/GRBLHal/index.html.br differ diff --git a/dist/CNC/GRBLHal/index.html.gz b/dist/CNC/GRBLHal/index.html.gz index 6805b7548..c5a3c8cb5 100644 Binary files a/dist/CNC/GRBLHal/index.html.gz and b/dist/CNC/GRBLHal/index.html.gz differ diff --git a/dist/Printer3D/Marlin-embedded/index.html.br b/dist/Printer3D/Marlin-embedded/index.html.br index d6c300257..9dd28ad5b 100644 Binary files a/dist/Printer3D/Marlin-embedded/index.html.br and b/dist/Printer3D/Marlin-embedded/index.html.br differ diff --git a/dist/Printer3D/Marlin-embedded/index.html.gz b/dist/Printer3D/Marlin-embedded/index.html.gz index 027302787..37c3027f7 100644 Binary files a/dist/Printer3D/Marlin-embedded/index.html.gz and b/dist/Printer3D/Marlin-embedded/index.html.gz differ diff --git a/dist/Printer3D/Marlin/index.html.br b/dist/Printer3D/Marlin/index.html.br index 4e7401540..6d71596a0 100644 Binary files a/dist/Printer3D/Marlin/index.html.br and b/dist/Printer3D/Marlin/index.html.br differ diff --git a/dist/Printer3D/Marlin/index.html.gz b/dist/Printer3D/Marlin/index.html.gz index fcb4587a4..21f4a7804 100644 Binary files a/dist/Printer3D/Marlin/index.html.gz and b/dist/Printer3D/Marlin/index.html.gz differ diff --git a/dist/Printer3D/Repetier/index.html.br b/dist/Printer3D/Repetier/index.html.br index 0ba978771..9e8d9795d 100644 Binary files a/dist/Printer3D/Repetier/index.html.br and b/dist/Printer3D/Repetier/index.html.br differ diff --git a/dist/Printer3D/Repetier/index.html.gz b/dist/Printer3D/Repetier/index.html.gz index 1600d397b..9c0063b8b 100644 Binary files a/dist/Printer3D/Repetier/index.html.gz and b/dist/Printer3D/Repetier/index.html.gz differ diff --git a/dist/Printer3D/Smoothieware/index.html.br b/dist/Printer3D/Smoothieware/index.html.br index 47ea47cf3..67063198e 100644 Binary files a/dist/Printer3D/Smoothieware/index.html.br and b/dist/Printer3D/Smoothieware/index.html.br differ diff --git a/dist/Printer3D/Smoothieware/index.html.gz b/dist/Printer3D/Smoothieware/index.html.gz index 48856d660..943dc1b8b 100644 Binary files a/dist/Printer3D/Smoothieware/index.html.gz and b/dist/Printer3D/Smoothieware/index.html.gz differ diff --git a/dist/SandTable/GRBL/index.html.br b/dist/SandTable/GRBL/index.html.br index ea061a2c4..9566ce3d8 100644 Binary files a/dist/SandTable/GRBL/index.html.br and b/dist/SandTable/GRBL/index.html.br differ diff --git a/dist/SandTable/GRBL/index.html.gz b/dist/SandTable/GRBL/index.html.gz index de5c961c2..fe4796539 100644 Binary files a/dist/SandTable/GRBL/index.html.gz and b/dist/SandTable/GRBL/index.html.gz differ diff --git a/src/components/Helpers/filters.js b/src/components/Helpers/filters.js index c2e6497c7..d4e15b7b9 100644 --- a/src/components/Helpers/filters.js +++ b/src/components/Helpers/filters.js @@ -69,7 +69,7 @@ const filterResultFiles = (files, path) => { //it is file or subfile ? if (newpath.indexOf("/") == -1 && newpath.length > 0) { //file - acc.push({ name: newpath, size: element.size }) + acc.push({ ...element, name: newpath }) } else { //subdir const foldername = newpath.substring( diff --git a/src/components/Panels/Files.js b/src/components/Panels/Files.js index 454081180..95a76bb6f 100644 --- a/src/components/Panels/Files.js +++ b/src/components/Panels/Files.js @@ -40,6 +40,7 @@ import { CornerRightUp, Edit3, XCircle, + ArrowUpCircle, } from "preact-feather" import { files, processor, useTargetContextFn } from "../../targets" import { Folder, File, Trash2, Play } from "preact-feather" @@ -50,6 +51,28 @@ const currentPath = {} const filesListCache = {} let currentFSNeedInit = true +const TransferProgress = ({ controller }) => { + const [transfer, setTransfer] = useState({ + progress: 0, + status: "starting", + }) + + useEffect(() => { + controller.update = (status) => setTransfer(status) + return () => { + delete controller.update + } + }, []) + + return ( +
+ +
{transfer.progress || 0}%
+
{transfer.status || T("P131")}
+
+ ) +} + /* * Local const * @@ -74,11 +97,16 @@ const FilesPanel = () => { const [fileSystem, setFileSystem] = useState(currentFS) const [filesList, setFilesList] = useState(filesListCache[currentFS]) const [menu, setMenu] = useState(null) + const [, setCapabilitiesRevision] = useState(0) const { createNewRequest, abortRequest } = useHttpFn const { processData } = useTargetContextFn const { modals, toasts } = useUiContext() + const modalsRef = useRef(modals) + modalsRef.current = modals const fileref = useRef() const dropRef = useRef() + const transferPollTimer = useRef() + const transferProgress = useRef({}).current const progressBar = {} //console.log("currentFS", currentFS) //console.log(currentFS) @@ -91,6 +119,195 @@ const FilesPanel = () => { setFilesList(filesListCache[currentFS]) } + useEffect(() => { + return () => clearTimeout(transferPollTimer.current) + }, []) + + useEffect(() => { + if (!files.subscribeCapabilities) return + return files.subscribeCapabilities(() => + setCapabilitiesRevision((revision) => revision + 1) + ) + }, []) + + const parseTransferStatus = (result) => { + try { + return typeof result == "string" ? JSON.parse(result) : result + } catch (error) { + console.log("Invalid Binary File Transfer status", error) + return null + } + } + + const closeTransferModal = () => { + const index = modalsRef.current.getModalIndex("progression") + if (index != -1) modalsRef.current.removeModal(index) + } + + const finishTransfer = (status) => { + clearTimeout(transferPollTimer.current) + if (transferProgress.update) transferProgress.update(status) + if (status.status == "completed") { + filesListCache.SD = undefined + toasts.addToast({ content: T("P128"), type: "success" }) + } else if (status.status == "cancelled") { + toasts.addToast({ content: T("P130"), type: "warning" }) + } else { + toasts.addToast({ + content: T("P129").replace( + "%s", + status.error || status.status || T("S22") + ), + type: "error", + }) + } + setTimeout(closeTransferModal, 500) + } + + const pollTransfer = () => { + createNewRequest( + espHttpURL("printer-sd-transfer", { action: "status" }), + { method: "GET", id: "marlin-bft-status", max: 1 }, + { + onSuccess: (result) => { + const status = parseTransferStatus(result) + if (!status) { + finishTransfer({ + status: "failed", + error: T("S4"), + progress: 0, + }) + } else if (status.active) { + if (transferProgress.update) + transferProgress.update(status) + transferPollTimer.current = setTimeout( + pollTransfer, + 500 + ) + } else { + finishTransfer(status) + } + }, + onFail: (error) => { + finishTransfer({ + status: "failed", + error, + progress: 0, + }) + }, + } + ) + } + + const cancelTransfer = () => { + clearTimeout(transferPollTimer.current) + createNewRequest( + espHttpURL("printer-sd-transfer", { action: "cancel" }), + { method: "POST", id: "marlin-bft-cancel" }, + { + onSuccess: () => { + transferPollTimer.current = setTimeout(pollTransfer, 250) + }, + onFail: (error) => { + toasts.addToast({ content: error, type: "error" }) + }, + } + ) + } + + const startTransfer = (source, destination) => { + showProgressModal({ + modals, + title: T("P127"), + button1: { cb: cancelTransfer, text: T("S28") }, + content: , + }) + createNewRequest( + espHttpURL("printer-sd-transfer", { + action: "start", + source, + destination, + compression: "auto", + }), + { method: "POST", id: "marlin-bft-start" }, + { + onSuccess: (result) => { + const status = parseTransferStatus(result) + if (status && status.active) { + if (transferProgress.update) + transferProgress.update(status) + transferPollTimer.current = setTimeout( + pollTransfer, + 250 + ) + } else { + finishTransfer( + status || { + status: "failed", + error: T("S4"), + progress: 0, + } + ) + } + }, + onFail: (error) => { + finishTransfer({ + status: "failed", + error, + progress: 0, + }) + }, + } + ) + } + + const showTransferModal = (element) => { + const command = files.command( + currentFS, + "transferToPrinter", + currentPath[currentFS], + element.name + ) + let destination = "/" + element.name + showModal({ + modals, + id: "marlinBftDestination", + title: T("P125"), + icon: , + content: ( + +
{T("P126")}
+ { + destination = e.target.value.trim() + }} + /> +
+ ), + button1: { + text: T("S27"), + cb: () => { + if ( + !destination.startsWith("/") || + destination.includes("..") || + destination.length > 240 || + /[\r\n]/.test(destination) + ) { + toasts.addToast({ + content: T("P132"), + type: "error", + }) + return + } + startTransfer(command.source, destination) + }, + }, + button2: { text: T("S28") }, + }) + } + const sendSerialCmd = (command) => { const cmds = command.split(";") cmds.forEach((cmd) => { @@ -426,7 +643,7 @@ const FilesPanel = () => { currentFS, element.size == -1 ? "deletedir" : "delete", currentPath[currentFS], - element.name + element.commandName || element.name ) if (cmd.type == "url") { sendURLCmd(cmd) @@ -514,37 +731,59 @@ const FilesPanel = () => { } setIsLoading(false) } else { - const cmd = files.command(currentFS, "list", currentPath[currentFS]) - if (cmd.type == "url") { - createNewRequest( - espHttpURL(cmd.url, cmd.args), - { method: "GET" }, - { - onSuccess: (result) => { - filesListCache[currentFS] = files.command( - currentFS, - "formatResult", - result - ) - setFilesList(filesListCache[currentFS]) - setIsLoading(false) - }, - onFail: (error) => { - console.log(error) - setIsLoading(false) - toasts.addToast({ content: error, type: "error" }) - }, - } + const runListCommand = () => { + const cmd = files.command( + currentFS, + "list", + currentPath[currentFS] ) - } else if (cmd.type == "cmd") { + if (cmd.type == "url") { + createNewRequest( + espHttpURL(cmd.url, cmd.args), + { method: "GET" }, + { + onSuccess: (result) => { + filesListCache[currentFS] = files.command( + currentFS, + "formatResult", + result + ) + setFilesList(filesListCache[currentFS]) + setIsLoading(false) + }, + onFail: (error) => { + console.log(error) + setIsLoading(false) + toasts.addToast({ + content: error, + type: "error", + }) + }, + } + ) + } else if (cmd.type == "cmd") { + if ( + processor.startCatchResponse( + currentFS, + "list", + processFeedback + ) + ) + sendSerialCmd(cmd.cmd) + } + } + const preflight = files.command(currentFS, "preflightList") + if (preflight.type == "cmd") { if ( processor.startCatchResponse( currentFS, - "list", - processFeedback + "preflightList", + () => runListCommand() ) ) - sendSerialCmd(cmd.cmd) + sendSerialCmd(preflight.cmd) + } else { + runListCommand() } } } @@ -833,6 +1072,26 @@ const FilesPanel = () => { {line.size != -1 && (
{line.size}
+ {files.capability( + currentFS, + "TransferToPrinter", + currentPath[currentFS], + line.name + ) && ( + } + onClick={(e) => { + e.target.blur() + useUiContextFn.haptic() + showTransferModal( + line + ) + }} + /> + )} {files.capability( currentFS, "Process", @@ -854,7 +1113,8 @@ const FilesPanel = () => { currentPath[ currentFS ], - line.name + line.commandName || + line.name ) sendSerialCmd( cmd.cmd @@ -867,7 +1127,15 @@ const FilesPanel = () => { "Process", currentPath[currentFS], line.name - ) &&
} + ) && + !files.capability( + currentFS, + "TransferToPrinter", + currentPath[currentFS], + line.name + ) && ( +
+ )} )} {files.capability( diff --git a/src/components/Panels/Terminal.js b/src/components/Panels/Terminal.js index a7bbf187a..107302d78 100644 --- a/src/components/Panels/Terminal.js +++ b/src/components/Panels/Terminal.js @@ -25,10 +25,16 @@ import { CheckCircle, Circle, PauseCircle, + AlertTriangle, ChevronLeft, ChevronRight, } from "preact-feather" -import { useUiContext, useDatasContext, useUiContextFn } from "../../contexts" +import { + useUiContext, + useDatasContext, + useUiContextFn, + useSettingsContext, +} from "../../contexts" import { useTargetContext, variablesList } from "../../targets" import { useHttpQueue } from "../../hooks" import { espHttpURL, replaceVariables } from "../Helpers" @@ -43,6 +49,7 @@ const TerminalPanel = () => { const { panels, uisettings } = useUiContext() const { terminal } = useDatasContext() const { processData } = useTargetContext() + const { printerLink } = useSettingsContext() const { createNewRequest } = useHttpQueue() if (terminal.isVerbose.current == undefined) terminal.isVerbose.current = uisettings.getValue("verbose") @@ -106,6 +113,7 @@ const TerminalPanel = () => { } } const onSend = (e) => { + if (printerLink.captured) return useUiContextFn.haptic() inputRef.current.focus() if (!terminal.input.current && variablesList.allowEmptyLine) @@ -253,6 +261,7 @@ const TerminalPanel = () => { ref={inputRef} value={terminal.input.current} placeholder={T("S80")} + disabled={printerLink.captured} /> { label={T("S81")} icon={} onClick={onSend} + disabled={printerLink.captured} />
+ {printerLink.captured && ( +
+ + + {T("S261").replace( + "%s", + printerLink.owner || T("S19") + )} + +
+ )}
{ const connectionValues = useRef({}) const featuresValues = useRef({}) const pollingInterval = useRef([]) + const printerLinkCaptured = useRef(false) + const [printerLink, setPrinterLink] = useState({ + captured: false, + owner: "", + }) useSettingsContextFn.getValue = (val) => connectionValues.current[val] function startPolling(id, interval, fn) { stopPolling(id) if (interval > 0 && fn) { const newInterval = setInterval(() => { - fn() + if (!printerLinkCaptured.current) fn() }, interval) pollingInterval.current.push({ id: id, interval: newInterval }) } } + function setPrinterLinkState(captured, owner = "") { + printerLinkCaptured.current = captured + setPrinterLink({ captured, owner: captured ? owner : "" }) + } + /* * Stop polling query */ @@ -72,6 +82,8 @@ const SettingsContextProvider = ({ children }) => { connectionSettings: connectionValues, featuresSettings: featuresValues, activity: { startPolling, stopPolling }, + printerLink, + setPrinterLinkState, } return ( diff --git a/src/contexts/UiContext.js b/src/contexts/UiContext.js index 037946f03..aa44976ae 100644 --- a/src/contexts/UiContext.js +++ b/src/contexts/UiContext.js @@ -55,6 +55,8 @@ const UiContextProvider = ({ children }) => { page: "connecting", }) const [uiSetup, setUiSetup] = useState(false) + const modalsRef = useRef(modals) + modalsRef.current = modals const toastsRef = useRef(toasts) toastsRef.current = toasts const notificationsRef = useRef(notifications) @@ -123,19 +125,21 @@ const UiContextProvider = ({ children }) => { } const addModal = (newModal) => - setModal([ - ...modals, + setModal((currentModals) => [ + ...currentModals, { ...newModal, id: newModal.id ? newModal.id : generateUID() }, ]) const getModalIndex = (id) => { - return modals.findIndex((element) => element.id == id) + return modalsRef.current.findIndex((element) => element.id == id) } const removeModal = (modalIndex) => { - const newModalList = modals.filter( - (modal, index) => index !== modalIndex - ) - setModal(newModalList) - if (newModalList.length == 0) disableUI(false) + setModal((currentModals) => { + const newModalList = currentModals.filter( + (modal, index) => index !== modalIndex + ) + if (newModalList.length == 0) disableUI(false) + return newModalList + }) } const clearModals = () => { diff --git a/src/contexts/WsContext.js b/src/contexts/WsContext.js index 3481107b4..c2ecfdc9e 100644 --- a/src/contexts/WsContext.js +++ b/src/contexts/WsContext.js @@ -42,7 +42,8 @@ const WsContextProvider = ({ children }) => { const { toasts, connection, dialogs, modals } = useUiContext() const { removeAllRequests } = useHttpQueueContext() const dataBuffer = useRef([]) - const { connectionSettings, activity } = useSettingsContext() + const { connectionSettings, activity, setPrinterLinkState } = + useSettingsContext() const wsConnection = useRef() const [isPingPaused, setIsPingPaused] = useState(false) const [isPingStarted, setIsPingStarted] = useState(false) @@ -90,6 +91,16 @@ const WsContextProvider = ({ children }) => { Disconnect("already connected") } break + case "PRINTERLINK": + if (eventLine[1] == "captured") { + setPrinterLinkState( + true, + eventLine.slice(2).join(":") + ) + } else if (eventLine[1] == "released") { + setPrinterLinkState(false) + } + break case "PING": if (eventLine.length == 3) { if (eventLine[1] <= 0) { @@ -156,6 +167,7 @@ const WsContextProvider = ({ children }) => { } //stop polling if any activity.stopPolling() + setPrinterLinkState(false) //Abort / Remove all queries removeAllRequests() //Clear all opened modals diff --git a/src/targets/Printer3D/Marlin/DIRECTSD-source.js b/src/targets/Printer3D/Marlin/DIRECTSD-source.js index a162e02c5..b0666daa6 100644 --- a/src/targets/Printer3D/Marlin/DIRECTSD-source.js +++ b/src/targets/Printer3D/Marlin/DIRECTSD-source.js @@ -21,6 +21,7 @@ import { h } from "preact" import { canProcessFile } from "../../helpers" import { sortedFilesList, formatStatus } from "../../../components/Helpers" import { useUiContextFn, useSettingsContextFn } from "../../../contexts" +import { getPrinterCapability } from "./printerCapabilities" const capabilities = { Process: (path, filename) => { @@ -55,6 +56,12 @@ const capabilities = { CreateDir: () => { return true }, + TransferToPrinter: () => { + return ( + useSettingsContextFn.getValue("SDConnection") == "direct" && + getPrinterCapability("BINARY_FILE_TRANSFER") == "1" + ) + }, } const commands = { @@ -124,6 +131,15 @@ const commands = { args: {}, } }, + transferToPrinter: (path, filename) => { + const source = ( + "/SD" + + path + + (path.endsWith("/") ? "" : "/") + + filename + ).replaceAll("//", "/") + return { type: "bft", source } + }, play: (path, filename) => { if ( useSettingsContextFn.getValue("Streaming") == "Enabled" && diff --git a/src/targets/Printer3D/Marlin/FLASH-source.js b/src/targets/Printer3D/Marlin/FLASH-source.js index 494d0ac1c..d73538ab4 100644 --- a/src/targets/Printer3D/Marlin/FLASH-source.js +++ b/src/targets/Printer3D/Marlin/FLASH-source.js @@ -21,6 +21,7 @@ import { h } from "preact" import { sortedFilesList, formatStatus } from "../../../components/Helpers" import { canProcessFile } from "../../helpers" import { useUiContextFn, useSettingsContextFn } from "../../../contexts" +import { getPrinterCapability } from "./printerCapabilities" const capabilities = { Process: (path, filename) => { if (useSettingsContextFn.getValue("Streaming") == "Enabled") { @@ -51,6 +52,9 @@ const capabilities = { CreateDir: () => { return true }, + TransferToPrinter: () => { + return getPrinterCapability("BINARY_FILE_TRANSFER") == "1" + }, } const commands = { @@ -115,6 +119,15 @@ const commands = { args: {}, } }, + transferToPrinter: (path, filename) => { + const source = ( + "/FS" + + path + + (path.endsWith("/") ? "" : "/") + + filename + ).replaceAll("//", "/") + return { type: "bft", source } + }, play: (path, filename) => { if ( useSettingsContextFn.getValue("Streaming") == "Enabled" && diff --git a/src/targets/Printer3D/Marlin/SD-source.js b/src/targets/Printer3D/Marlin/SD-source.js index e77b23774..e38c0754d 100644 --- a/src/targets/Printer3D/Marlin/SD-source.js +++ b/src/targets/Printer3D/Marlin/SD-source.js @@ -26,6 +26,17 @@ import { filterResultFiles, } from "../../../components/Helpers" import { useUiContextFn, useSettingsContextFn } from "../../../contexts" +import { getPrinterCapability } from "./printerCapabilities" + +let longFilenameCapabilityChecked = false + +const supportsLongFilenames = () => + getPrinterCapability("LONG_FILENAME") == "1" + +const listCommand = () => { + const cmd = useUiContextFn.getValue("sdlistcmd") + return supportsLongFilenames() ? `${cmd} L` : cmd +} //Extract information from string - specific to FW / source const formatFileSerialLine = (lines) => { @@ -44,6 +55,7 @@ const formatFileSerialLine = (lines) => { ...acc, { name: pathAlt || path, + commandName: path, size: formatFileSizeToString(sizeAlt || size), }, ] @@ -82,10 +94,14 @@ const capabilities = { } const commands = { + preflightList: () => { + if (longFilenameCapabilityChecked) return { type: "none" } + return { type: "cmd", cmd: "M115" } + }, list: (path, filename) => { return { type: "cmd", - cmd: useUiContextFn.getValue("sdlistcmd"), + cmd: listCommand(), } }, upload: (path, filename) => { @@ -98,7 +114,7 @@ const commands = { //other is not supported so return list command for safety return { type: "cmd", - cmd: useUiContextFn.getValue("sdlistcmd"), + cmd: listCommand(), } }, postUpload: (path, filename) => { @@ -129,10 +145,12 @@ const commands = { return res }, play: (path, filename) => { - const spath = (path + (path == "/" ? "" : "/") + filename).replaceAll( - "//", - "/" - ) + const spath = filename.includes("/") + ? `/${filename.replace(/^\/+/, "")}` + : (path + (path == "/" ? "" : "/") + filename).replaceAll( + "//", + "/" + ) const cmd = useUiContextFn.getValue("sdplaycmd").replace("#", spath) return { type: "cmd", @@ -140,10 +158,12 @@ const commands = { } }, delete: (path, filename) => { - const spath = (path + (path == "/" ? "" : "/") + filename).replaceAll( - "//", - "/" - ) + const spath = filename.includes("/") + ? `/${filename.replace(/^\/+/, "")}` + : (path + (path == "/" ? "" : "/") + filename).replaceAll( + "//", + "/" + ) const cmd = useUiContextFn.getValue("sddeletecmd").replace("#", spath) return { type: "cmd", @@ -153,6 +173,21 @@ const commands = { } const responseSteps = { + preflightList: { + start: (data) => data.startsWith("FIRMWARE_NAME:"), + end: (data) => { + const ended = data.startsWith("ok") && !data.startsWith("ok T:") + if (ended) longFilenameCapabilityChecked = true + return ended + }, + error: (data) => { + const failed = + data.indexOf("error") != -1 || + data.indexOf("Unknown command") != -1 + if (failed) longFilenameCapabilityChecked = true + return failed + }, + }, list: { start: (data) => data.startsWith("Begin file list"), end: (data) => data.startsWith("End file list"), diff --git a/src/targets/Printer3D/Marlin/TargetContext.js b/src/targets/Printer3D/Marlin/TargetContext.js index 0d3db5347..12f895657 100644 --- a/src/targets/Printer3D/Marlin/TargetContext.js +++ b/src/targets/Printer3D/Marlin/TargetContext.js @@ -22,10 +22,13 @@ import { limitArr, dispatchToExtensions, beautifyJSONString, - addObjectItem, } from "../../../components/Helpers" import { useDatasContext, useUiContextFn } from "../../../contexts" import { processor } from "./processor" +import { + addPrinterCapabilities, + getPrinterCapability as getStoredPrinterCapability, +} from "./printerCapabilities" import { isVerboseOnly } from "./stream" import { isOk, @@ -65,7 +68,8 @@ useTargetContextFn.isStaId = (subsectionId, label, fieldData) => { return false } -const printerCapabilities = [] +useTargetContextFn.getPrinterCapability = (name) => + getStoredPrinterCapability(name) const TargetContextProvider = ({ children }) => { //format is x:value, y:value, z:value @@ -231,12 +235,7 @@ const TargetContextProvider = ({ children }) => { setFeedRate(feedsRate.current) } else if (isPrinterCapability(data)) { const res = getPrinterCapability(data) - res.forEach((cap) => { - addObjectItem(printerCapabilities, "name", { - name: cap.name, - value: cap.value, - }) - }) + addPrinterCapabilities(res) } else if (isSensor(data)) { const result = getSensor(data) setSensorData({ S: result }) diff --git a/src/targets/Printer3D/Marlin/files.js b/src/targets/Printer3D/Marlin/files.js index 918781d17..47e69eb53 100644 --- a/src/targets/Printer3D/Marlin/files.js +++ b/src/targets/Printer3D/Marlin/files.js @@ -25,6 +25,7 @@ import { SDEXT } from "./SDEXT-source" import { TFTSD } from "./TFT-SD-source" import { TFTUSB } from "./TFT-USB-source" import { useSettingsContextFn, useUiContextFn } from "../../../contexts" +import { subscribePrinterCapabilities } from "./printerCapabilities" //List of supported files systems const supportedFileSystems = [ @@ -124,6 +125,7 @@ const files = { command, capability, supported: supportedFileSystems, + subscribeCapabilities: subscribePrinterCapabilities, } export { files } diff --git a/src/targets/Printer3D/Marlin/printerCapabilities.js b/src/targets/Printer3D/Marlin/printerCapabilities.js new file mode 100644 index 000000000..4f31ed26e --- /dev/null +++ b/src/targets/Printer3D/Marlin/printerCapabilities.js @@ -0,0 +1,37 @@ +/* + printerCapabilities.js - ESP3D WebUI Target file + + Copyright (c) 2026 ESP3D WebUI contributors. All rights reserved. + + This code is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. +*/ +import { addObjectItem } from "../../../components/Helpers" + +// Keep this outside TargetContext: SD-source is reached through processor, +// which TargetContext imports, so importing TargetContext here would cycle. +const printerCapabilities = [] +const capabilityListeners = new Set() + +const addPrinterCapabilities = (capabilities) => { + capabilities.forEach((capability) => { + addObjectItem(printerCapabilities, "name", capability) + }) + capabilityListeners.forEach((listener) => listener()) +} + +const getPrinterCapability = (name) => + printerCapabilities.find((capability) => capability.name == name)?.value + +const subscribePrinterCapabilities = (listener) => { + capabilityListeners.add(listener) + return () => capabilityListeners.delete(listener) +} + +export { + addPrinterCapabilities, + getPrinterCapability, + subscribePrinterCapabilities, +} diff --git a/src/targets/Printer3D/translations/en.json b/src/targets/Printer3D/translations/en.json index 45ae81c05..1e8e45de6 100644 --- a/src/targets/Printer3D/translations/en.json +++ b/src/targets/Printer3D/translations/en.json @@ -117,6 +117,14 @@ "P122": "SD delete path command", "P123": "TFT SD delete path command", "P124": "TFT USB delete path command", + "P125": "Copy to printer SD", + "P126": "Destination path on printer SD", + "P127": "Copying to printer SD...", + "P128": "Transfer completed", + "P129": "Transfer failed: %s", + "P130": "Transfer cancelled", + "P131": "Starting transfer", + "P132": "Invalid destination path", "pause": "Pause", "processing": "Processing", "heating": "Heating", diff --git a/src/targets/translations/en.json b/src/targets/translations/en.json index 485fc0d7e..3653b4203 100644 --- a/src/targets/translations/en.json +++ b/src/targets/translations/en.json @@ -251,6 +251,7 @@ "S258": "px", "S259": "Panel min width", "S260": "Panel max width", + "S261": "Terminal unavailable: printer link is reserved by %s", "btn+X": "X+", "btn-X": "X-", "btnHX": "Home X",