Skip to content
Draft
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
71 changes: 71 additions & 0 deletions src/components/secondary/SettingsPopUp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const SettingsPopUp = ({
"CANVAS SETTINGS",
"PROJECT SETTINGS",
"RENDER PREFERENCES",
"USER SETTINGS",
];
function CustomTabPanel({ children, value, index }) {
return value === index ? (
Expand All @@ -81,6 +82,7 @@ const SettingsPopUp = ({
),
atomSize: Globalvariables.atomSize * 1000,
projectDescription: Globalvariables.currentAWSnode.description,
maslowIP: localStorage.getItem("maslowIP") || "",
});

const handleValueChange = (event) => {
Expand All @@ -102,6 +104,9 @@ const SettingsPopUp = ({
`${event.target.value}px Work Sans Bold`
);
}
if (event.target.name === "maslowIP") {
localStorage.setItem("maslowIP", event.target.value);
}
};

const handleCheckChange = (event) => {
Expand Down Expand Up @@ -440,6 +445,72 @@ const SettingsPopUp = ({
</span>
</div>
</CustomTabPanel>
<CustomTabPanel value={value} index={4}>
<div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<label
style={{ fontWeight: 500, marginBottom: 2 }}
htmlFor="maslow-ip"
>
Maslow Machine IP Address
</label>
<input
id="maslow-ip"
type="text"
value={state.maslowIP}
onChange={handleValueChange}
name="maslowIP"
placeholder="e.g., 192.168.1.100"
style={{
width: "100%",
padding: "8px",
borderRadius: 4,
border: "1px solid #ccc",
fontFamily: "inherit",
fontSize: 15,
}}
/>
<span
style={{
color: "#666",
fontSize: "12px",
display: "block",
marginTop: "4px",
}}
>
Enter your Maslow CNC machine's IP address to enable direct G-code uploads.
You can find this on the Maslow's display or in your router's connected devices list.
</span>
</div>
<div style={{ borderTop: "1px solid #eee", margin: "10px 0" }} />
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<label style={{ fontWeight: 500, marginBottom: 2 }}>
Upload Information
</label>
<span
style={{
color: "#666",
fontSize: "13px",
lineHeight: "1.5",
}}
>
Once configured, you'll be able to upload G-code directly to your Maslow machine from the G-code atom.
The machine must be on the same network and powered on.
</span>
<span
style={{
color: "#f57c00",
fontSize: "12px",
marginTop: "8px",
display: "block",
}}
>
⚠️ Note: Your browser may show a mixed content warning (HTTPS to HTTP).
You may need to allow insecure content for this feature to work.
</span>
</div>
</div>
</CustomTabPanel>
<div className="settings-panel-button-row">
<button className="settings-panel-button" type="submit">
Save Changes
Expand Down
119 changes: 119 additions & 0 deletions src/js/MaslowUploader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* MaslowUploader - Handles uploading G-code files to Maslow CNC machines
*
* This class provides methods to upload G-code files to Maslow machines via HTTP.
* The Maslow firmware (FluidNC) accepts POST requests to upload files to either
* SD card storage or local flash storage.
*/
class MaslowUploader {
constructor(maslowIP) {
this.maslowIP = maslowIP;
this.baseURL = `http://${maslowIP}`;
}

/**
* Upload a G-code file to the Maslow's SD card
* @param {Blob|File} file - The file to upload
* @param {string} filename - Desired filename on the Maslow
* @param {Function} onProgress - Optional progress callback (receives percent 0-100)
* @returns {Promise<Object>} Upload result
*/
async uploadToSD(file, filename, onProgress = null) {
return this._upload('/upload', file, filename, onProgress);
}

/**
* Upload a file to the Maslow's local filesystem
* @param {Blob|File} file - The file to upload
* @param {string} filename - Desired filename on the Maslow
* @param {Function} onProgress - Optional progress callback (receives percent 0-100)
* @returns {Promise<Object>} Upload result
*/
async uploadToLocalFS(file, filename, onProgress = null) {
return this._upload('/files', file, filename, onProgress);
}

_upload(endpoint, file, filename, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();

// Add the file and size parameter
formData.append(filename, file, filename);
formData.append(`${filename}S`, file.size.toString());

// Setup progress tracking
if (onProgress && typeof onProgress === 'function') {
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
onProgress(percentComplete);
}
});
}

// Handle completion
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
try {
const result = JSON.parse(xhr.responseText);
if (result.status && result.status.includes('failed')) {
reject(new Error(result.status));
} else {
resolve(result);
}
} catch (e) {
reject(new Error('Invalid response from Maslow'));
}
} else if (xhr.status === 401) {
reject(new Error('Authentication required. Please log in to the Maslow web interface first.'));
} else {
reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
}
});

// Handle errors
xhr.addEventListener('error', () => {
reject(new Error('Network error. Please check:\n- The Maslow is powered on and connected\n- The IP address is correct\n- Your firewall allows connections\n- You are on the same network'));
});

xhr.addEventListener('abort', () => {
reject(new Error('Upload cancelled'));
});

// Send the request
xhr.open('POST', `${this.baseURL}${endpoint}`);
// Note: withCredentials removed to avoid CORS preflight issues with multiple headers
xhr.send(formData);
});
}

/**
* Test if the Maslow is reachable
* @returns {Promise<boolean>}
*/
async isReachable() {
try {
// Try HEAD request first (lightweight check)
// Note: credentials removed to avoid CORS preflight issues
const response = await fetch(`${this.baseURL}/`, {
method: 'HEAD',
mode: 'cors',
});
return response.ok;
} catch (e) {
// Fallback: Try GET request if HEAD is not supported
try {
const response = await fetch(`${this.baseURL}/`, {
method: 'GET',
mode: 'cors',
});
return response.ok;
} catch (fallbackError) {
return false;
}
}
}
}

export default MaslowUploader;
106 changes: 103 additions & 3 deletions src/molecules/gcode.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import Atom from "../prototypes/atom.js";
import GlobalVariables from "../js/globalvariables.js";
import MaslowUploader from "../js/MaslowUploader.js";

import { saveAs } from "file-saver";

// Error messages for G-code generation and upload
const ERROR_MESSAGES = {
NO_GCODE: "No G-code available. Please generate G-code first.",
NO_MASLOW_IP: "Please configure your Maslow IP address in Settings > User Settings first.",
CONFIGURE_INSTRUCTIONS: "To upload G-code directly to your Maslow:\n\n1. Open Settings (gear icon)\n2. Go to User Settings tab\n3. Enter your Maslow's IP address\n\nYou can find the IP on your Maslow's display or in your router's connected devices.",
CORS_BLOCKED: "Upload blocked by browser security.\n\nThis happens because:\n• Abundance uses HTTPS but Maslow uses HTTP\n• The browser blocks mixed HTTP/HTTPS requests\n\nTo fix this:\n1. Access Abundance via HTTP instead: http://abundance.maslowcnc.com\n2. Or enable insecure content in your browser for this site\n3. Or use localhost if running Abundance locally\n\nNote: This is a browser security feature to protect you.",
};

/**
* This class creates the circle atom.
*/
Expand Down Expand Up @@ -125,6 +134,18 @@ export default class Gcode extends Atom {
*/
this.sortDirection = "Left";

/**
* Upload progress tracking (0-100)
* @type {number}
*/
this.uploadProgress = 0;

/**
* Flag to track if upload is in progress
* @type {boolean}
*/
this.isUploading = false;

this.setValues(values);
}

Expand Down Expand Up @@ -704,16 +725,95 @@ export default class Gcode extends Atom {
const fileName = `${currentPartName}.gcode`;
this.downloadGcode(this.gcodeString, fileName);
} else {
console.warn("No G-code available. Please generate G-code first.");
// You could also show an alert or notification to the user here
alert("No G-code available. Please generate G-code first.");
console.warn(ERROR_MESSAGES.NO_GCODE);
alert(ERROR_MESSAGES.NO_GCODE);
}
},
};

// Add upload button if Maslow IP is configured
const maslowIP = localStorage.getItem("maslowIP");
if (maslowIP) {
inputParams[`Upload to Maslow SD - ${partName}`] = {
type: "button",
label: `Upload to Maslow SD`,
onClick: () => {
if (this.gcodeGenerated && this.gcodeString) {
const currentPartName =
this.findIOValue("Part Name") || this.partName || "output";
const fileName = `${currentPartName}.gcode`;
this.uploadToMaslow("SD", fileName);
} else {
alert(ERROR_MESSAGES.NO_GCODE);
}
},
};
} else {
// Show a hint button if IP is not configured
inputParams["Configure Maslow Upload"] = {
type: "button",
label: "Configure Maslow Upload",
onClick: () => {
alert(ERROR_MESSAGES.CONFIGURE_INSTRUCTIONS);
},
};
}

return inputParams;
}

/**
* Upload G-code to Maslow machine
* @param {string} uploadType - Either "SD" or "LocalFS"
* @param {string} filename - The filename for the uploaded file
*/
async uploadToMaslow(uploadType, filename = "output.gcode") {
if (!this.gcodeGenerated || !this.gcodeString) {
alert(ERROR_MESSAGES.NO_GCODE);
return;
}

// Get Maslow IP from localStorage
const maslowIP = localStorage.getItem("maslowIP");
if (!maslowIP) {
alert(ERROR_MESSAGES.NO_MASLOW_IP);
return;
}

this.isUploading = true;
this.uploadProgress = 0;

try {
const uploader = new MaslowUploader(maslowIP);

// Create blob from gcode string
const gcodeBlob = new Blob([this.gcodeString], { type: "text/plain" });

// Upload to SD card with progress tracking
// Note: Skipping reachability check to avoid CORS preflight issues
const result = await uploader.uploadToSD(gcodeBlob, filename, (percent) => {
this.uploadProgress = percent;
console.log(`Upload progress: ${percent.toFixed(1)}%`);
});

console.log("Upload successful:", result);
alert(`Successfully uploaded ${filename} to Maslow SD card!`);
} catch (error) {
console.error("Upload failed:", error);

// Check if error is due to CORS/mixed content blocking
const errorMsg = error.message.toLowerCase();
if (errorMsg.includes('network error') || errorMsg.includes('cors') || errorMsg.includes('cross-origin')) {
alert(ERROR_MESSAGES.CORS_BLOCKED);
} else {
alert(`Upload failed: ${error.message}`);
}
} finally {
this.isUploading = false;
this.uploadProgress = 0;
}
}

//Function to download G-code from a G-code string
downloadGcode(gcode, filename = "output.gcode") {
if (this.gcodeGenerated && !gcode) {
Expand Down