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 (
+
{
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",