Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions tests/start_gcode_helper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
Expand Down
13 changes: 8 additions & 5 deletions tools/start-gcode-helper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<target>` after the first `M140`, then looks for the
first `G0 Z5` after `M190` and adds this wait sequence beneath it:

Expand All @@ -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.
Expand Down
16 changes: 15 additions & 1 deletion tools/start-gcode-helper/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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.";
Expand All @@ -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();
Expand All @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion tools/start-gcode-helper/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ <h1>Add chamber control to your start G-code</h1>

<section class="card" aria-labelledby="upload-title">
<h2 id="upload-title">1. Add your start G-code</h2>
<p class="warning"><strong>Start from the original, unmodified slicer start G-code.</strong> 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.</p>
<label class="confirm-original"><input id="confirm-original" type="checkbox"> I’m using the original start G-code from my slicer, before adding these chamber commands.</label>
<label class="dropzone" id="dropzone" for="file-input">
<span class="upload-icon" aria-hidden="true">↑</span>
<span><strong>Choose a file</strong> or drop it here</span>
Expand All @@ -33,7 +35,7 @@ <h2 id="settings-title">2. Set the chamber target</h2>
<label for="target">Temperature command value</label>
<input id="target" type="text" value="{overall_chamber_temperature}" autocomplete="off" spellcheck="false">
<p class="hint">Keep the slicer variable for a start-G-code template. For an already-sliced file, enter its chamber temperature, for example <code>55</code>.</p>
<button id="process" type="button">Add chamber commands</button>
<button id="process" type="button" disabled>Add chamber commands</button>
<p id="status" role="status" aria-live="polite"></p>
</section>

Expand Down
51 changes: 10 additions & 41 deletions tools/start-gcode-helper/processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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")) {
Expand All @@ -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." : ".")
};
}
Expand Down
5 changes: 5 additions & 0 deletions tools/start-gcode-helper/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Loading