diff --git a/tests/start_gcode_helper.test.js b/tests/start_gcode_helper.test.js index c8062fd..c9a370a 100644 --- a/tests/start_gcode_helper.test.js +++ b/tests/start_gcode_helper.test.js @@ -19,12 +19,10 @@ assert.equal((result.text.match(/^M191 /gm) || []).length, 1); assert.match(result.text, /M140 .*\nM141 S\{overall_chamber_temperature\}/); assert.match(result.text, /G0 Z5 F10000\nM106 P2 S178\nM191 S\{overall_chamber_temperature\}\nM107 P2\n; WAIT_CHAMBER_TEMP TIMEOUT=180\nG28 Z/); -const updated = processGcode(result.text, "55"); -assert.equal((updated.text.match(/^M141 /gm) || []).length, 1); -assert.equal((updated.text.match(/^M191 /gm) || []).length, 1); -assert.match(updated.text, /M141 S55/); -assert.match(updated.text, /M106 P2 S178\nM191 S55\nM107 P2/); -assert.equal((updated.text.match(/^; WAIT_CHAMBER_TEMP/gm) || []).length, 1); +assert.throws(() => processGcode(result.text, "55"), /original, unmodified slicer start G-code/); +assert.throws(() => processGcode(template.replace("M140", "M140\nM141 S50"), "55"), /already contains DragonBreath chamber commands/); +assert.throws(() => processGcode(template.replace("G0 Z5 F10000", "G0 Z5 F10000\nM191 S50"), "55"), /already contains DragonBreath chamber commands/); +assert.throws(() => processGcode(template.replace("G0 Z5 F10000", "G0 Z5 F10000\nM106 P2 S178"), "55"), /already contains DragonBreath chamber commands/); const outsideFan = template + "\nM106 P2 S255\n"; assert.match(processGcode(outsideFan, "55").text, /M107 P2\n; WAIT_CHAMBER_TEMP TIMEOUT=180\nG28 Z[\s\S]*M106 P2 S255/); diff --git a/tools/start-gcode-helper/README.md b/tools/start-gcode-helper/README.md index ae6ab3a..91d56dc 100644 --- a/tools/start-gcode-helper/README.md +++ b/tools/start-gcode-helper/README.md @@ -4,6 +4,11 @@ A dependency-free static page for adding DragonBreath chamber control to a printer's start G-code. It runs completely in the browser; files are never uploaded to a server. +Start with the original, unmodified slicer start G-code. The helper cannot +prove that a file is otherwise unmodified: it only checks the startup section +for `M141`, `M191`, or the helper's `M106 P2 S178` fan command and stops if it +finds one. Users must confirm they started from the original slicer code. + The helper places `M141 S` after the first `M140`, then looks for the first `G0 Z5` after `M190` and adds this wait sequence beneath it: @@ -15,11 +20,9 @@ M107 P2 `S178` is 70% of the 0–255 fan scale. The target defaults to the Orca/Bambu template variable `{overall_chamber_temperature}`. For already-sliced G-code, -enter the numeric chamber target instead. If a chamber wait already exists in -the startup area, the helper updates it and its adjacent P2 fan commands rather -than adding a second wait. It also comments out an active `WAIT_CHAMBER_TEMP` -in that startup section because `M191` now performs the wait. Unrelated fan -commands later in the print are kept. +enter the numeric chamber target instead. It comments out an active +`WAIT_CHAMBER_TEMP` in that startup section because `M191` now performs the +wait. Unrelated fan commands later in the print are kept. The page expects start G-code containing `M140` and `M190`. Review the preview before downloading and using the updated file. diff --git a/tools/start-gcode-helper/app.js b/tools/start-gcode-helper/app.js index 7485667..0fd60fc 100644 --- a/tools/start-gcode-helper/app.js +++ b/tools/start-gcode-helper/app.js @@ -7,8 +7,16 @@ const filename = document.getElementById("filename"); const resultCard = document.getElementById("result-card"); const output = document.getElementById("output"); + const originalConfirmation = document.getElementById("confirm-original"); + const processButton = document.getElementById("process"); let downloadName = "start-gcode-updated.gcode"; + function clearOriginalConfirmation() { + originalConfirmation.checked = false; + processButton.disabled = true; + resultCard.hidden = true; + } + function showError(message) { status.textContent = message; status.classList.add("error"); @@ -24,6 +32,7 @@ const reader = new FileReader(); reader.onload = () => { source.value = String(reader.result || ""); + clearOriginalConfirmation(); downloadName = file.name.replace(/\.(?:gcode|gco|gc|g|txt)$/i, "") + "-updated.gcode"; filename.textContent = file.name; status.textContent = "File loaded locally. Review the text, then add the commands."; @@ -35,6 +44,7 @@ } fileInput.addEventListener("change", () => readFile(fileInput.files[0])); + source.addEventListener("input", clearOriginalConfirmation); for (const eventName of ["dragenter", "dragover"]) { dropzone.addEventListener(eventName, (event) => { event.preventDefault(); @@ -49,7 +59,11 @@ } dropzone.addEventListener("drop", (event) => readFile(event.dataTransfer.files[0])); - document.getElementById("process").addEventListener("click", () => { + originalConfirmation.addEventListener("change", () => { + processButton.disabled = !originalConfirmation.checked; + }); + + processButton.addEventListener("click", () => { try { const result = window.StartGcodeProcessor.processGcode(source.value, target.value); output.value = result.text; diff --git a/tools/start-gcode-helper/index.html b/tools/start-gcode-helper/index.html index 153eae8..37fe96d 100644 --- a/tools/start-gcode-helper/index.html +++ b/tools/start-gcode-helper/index.html @@ -17,6 +17,8 @@

Add chamber control to your start G-code

1. Add your start G-code

+

Start from the original, unmodified slicer start G-code. This helper cannot prove a file is otherwise unmodified. It only checks the startup section for existing DragonBreath chamber commands and stops if it finds them.

+
diff --git a/tools/start-gcode-helper/processor.js b/tools/start-gcode-helper/processor.js index 4ca4c71..0d1e6b4 100644 --- a/tools/start-gcode-helper/processor.js +++ b/tools/start-gcode-helper/processor.js @@ -13,14 +13,6 @@ return new RegExp("^" + code + "(?:\\s|$)", "i").test(command(line)); } - function isP2FanOn(line) { - return /^M106\s+.*\bP2\b/i.test(command(line)); - } - - function isP2FanOff(line) { - return /^M107\s+.*\bP2\b/i.test(command(line)) || /^M106\s+.*\bP2\b.*\bS0(?:\.0+)?\b/i.test(command(line)); - } - function isSafeTemperature(value) { const text = String(value).trim(); if (/^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(text)) return true; @@ -45,45 +37,22 @@ if (bedWaitIndex < 0) bedWaitIndex = lines.findIndex((line) => isCommand(line, "M190")); if (bedWaitIndex < 0) throw new Error("Couldn’t find M190, which marks the bed wait. The file was left unchanged."); - const beforeBedWait = lines.slice(0, bedWaitIndex); - const existingM141 = beforeBedWait.findIndex((line) => isCommand(line, "M141")); - if (existingM141 >= 0) { - lines[existingM141] = "M141 S" + target; - for (let i = beforeBedWait.length - 1; i > existingM141; i--) { - if (isCommand(lines[i], "M141")) lines.splice(i, 1); - } - } else { - lines.splice(bedTargetIndex + 1, 0, "M141 S" + target); - } - bedWaitIndex = lines.findIndex((line, index) => index > bedTargetIndex && isCommand(line, "M190")); - if (bedWaitIndex < 0) bedWaitIndex = lines.findIndex((line) => isCommand(line, "M190")); - let endIndex = lines.findIndex((line, index) => index > bedWaitIndex && (isCommand(line, "G28") || isCommand(line, "BED_MESH_CALIBRATE"))); if (endIndex < 0) endIndex = lines.length; - const safeZIndex = lines.findIndex((line, index) => index > bedWaitIndex && index < endIndex && /^G0\s+Z5(?:\.0+)?(?:\s|$)/i.test(command(line))); - // Replace only an existing chamber-wait block in the startup area. Fan - // commands elsewhere in the print are left alone. - const existingWaits = []; - for (let i = bedWaitIndex + 1; i < endIndex; i++) if (isCommand(lines[i], "M191")) existingWaits.push(i); - let insertAt = safeZIndex >= 0 ? safeZIndex + 1 : bedWaitIndex + 1; - if (existingWaits.length) { - insertAt = safeZIndex >= 0 ? safeZIndex + 1 : existingWaits[0]; - const remove = new Set(); - for (const index of existingWaits) { - remove.add(index); - if (index > bedWaitIndex + 1 && isP2FanOn(lines[index - 1])) remove.add(index - 1); - if (index + 1 < endIndex && isP2FanOff(lines[index + 1])) remove.add(index + 1); - } - for (const index of [...remove].sort((a, b) => b - a)) { - lines.splice(index, 1); - if (index < insertAt) insertAt--; - if (index < endIndex) endIndex--; - } + const alreadyHasChamberTarget = lines.slice(0, endIndex).some((line) => isCommand(line, "M141")); + const alreadyHasChamberWait = lines.slice(0, endIndex).some((line) => isCommand(line, "M191")); + const alreadyHasHelperFan = lines.slice(bedWaitIndex + 1, endIndex).some((line) => /^M106\s+P2\s+S178(?:\s|$)/i.test(command(line))); + if (alreadyHasChamberTarget || alreadyHasChamberWait || alreadyHasHelperFan) { + throw new Error("This start G-code already contains DragonBreath chamber commands. Use the original, unmodified slicer start G-code; do not run this helper on code that has already been modified."); } + lines.splice(bedTargetIndex + 1, 0, "M141 S" + target); + if (bedTargetIndex < bedWaitIndex) bedWaitIndex++; endIndex = lines.findIndex((line, index) => index > bedWaitIndex && (isCommand(line, "G28") || isCommand(line, "BED_MESH_CALIBRATE"))); if (endIndex < 0) endIndex = lines.length; + const safeZIndex = lines.findIndex((line, index) => index > bedWaitIndex && index < endIndex && /^G0\s+Z5(?:\.0+)?(?:\s|$)/i.test(command(line))); + let insertAt = safeZIndex >= 0 ? safeZIndex + 1 : bedWaitIndex + 1; let legacyWaitsCommented = 0; for (let i = bedWaitIndex + 1; i < endIndex; i++) { if (isCommand(lines[i], "WAIT_CHAMBER_TEMP")) { @@ -97,7 +66,7 @@ if (hadTrailingNewline) output += newline; return { text: output, - message: (existingWaits.length ? "Updated the chamber-wait block" : "Added the chamber target and 70% fan-assisted chamber wait") + + message: "Added the chamber target and 70% fan-assisted chamber wait" + (legacyWaitsCommented ? " and commented out the old WAIT_CHAMBER_TEMP." : ".") }; } diff --git a/tools/start-gcode-helper/style.css b/tools/start-gcode-helper/style.css index 60f667a..72ec015 100644 --- a/tools/start-gcode-helper/style.css +++ b/tools/start-gcode-helper/style.css @@ -37,6 +37,11 @@ textarea { min-height: 190px; resize: vertical; padding: 14px; tab-size: 2; } input[type="text"] { padding: 12px; } textarea:focus, input:focus { outline: 2px solid #559ee3; outline-offset: 1px; } .hint { margin: 8px 0 18px; font-size: .89rem; } +.warning { margin: 0 0 16px; padding: 12px 14px; border: 1px solid #806333; border-radius: 10px; background: #332916; color: #ffe1a6; } +.warning strong { color: #fff0cc; } +.confirm-original { display: flex; align-items: start; gap: 10px; margin: 0 0 12px; font-weight: 500; } +.confirm-original input { width: 18px; height: 18px; flex: 0 0 auto; margin: 2px 0 0; accent-color: #3185d5; } +button:disabled { background: #344253; color: #a6b1be; cursor: not-allowed; } code { color: #c7e2ff; } button { min-height: 46px; padding: 11px 18px; border: 1px solid transparent; border-radius: 10px; background: #3185d5; color: white; font: inherit; font-weight: 750; cursor: pointer; } button:hover { background: #4499e9; }