diff --git a/editor/index.html b/editor/index.html index 0afafba..7538e57 100755 --- a/editor/index.html +++ b/editor/index.html @@ -116,6 +116,7 @@

p5.fab

+ source diff --git a/examples/2.5D-sketching/sketch.js b/examples/2.5D-sketching/sketch.js index 2e5bc14..7266364 100644 --- a/examples/2.5D-sketching/sketch.js +++ b/examples/2.5D-sketching/sketch.js @@ -10,17 +10,23 @@ function setup() { fab.serial.requestPort(); // choose the serial port to connect to }); - let printButton = createButton("print!"); - printButton.position(20, 60); - printButton.mousePressed(function () { - fab.print(); // start streaming the commands to printer + let startButton = createButton('start!'); + startButton.position(20, 60); + startButton.mousePressed(function () { + fab.print(); // start streaming commands to printer }); - let stopButton = createButton("stop!"); + let stopButton = createButton('stop!'); stopButton.position(20, 100); stopButton.mousePressed(function () { fab.stopPrint(); // stop streaming the commands to printer }); + + let exportButton = createButton('export!'); + exportButton.position(20, 140); + exportButton.mousePressed(function () { + fab.exportGcode(); // export gcode to a file. + }); } function fabDraw() { @@ -52,6 +58,7 @@ function fabDraw() { r -= 0.1; } fab.presentPart(); + //console.log(fab.commands); } function draw() { diff --git a/examples/control-panel/sketch.js b/examples/control-panel/sketch.js index 3c94670..4ef087c 100644 --- a/examples/control-panel/sketch.js +++ b/examples/control-panel/sketch.js @@ -13,7 +13,7 @@ function fabDraw() { } function connectPrinter() { - fab.serial.requestPort(); + fab.connectPrinter(); } function bSend(dir) { diff --git a/examples/hollow-cube/sketch.js b/examples/hollow-cube/sketch.js index 555ca43..668dfff 100644 --- a/examples/hollow-cube/sketch.js +++ b/examples/hollow-cube/sketch.js @@ -21,6 +21,12 @@ function setup() { stopButton.mousePressed(function() { fab.stopPrint(); // stop streaming the commands to printer }); + + let exportButton = createButton('export!'); + exportButton.position(20, 140); + exportButton.mousePressed(function() { + fab.exportGcode(); // export gcode to a file. + }); } function fabDraw() { diff --git a/examples/line-vase/sketch.js b/examples/line-vase/sketch.js index 5d9820f..04d3539 100644 --- a/examples/line-vase/sketch.js +++ b/examples/line-vase/sketch.js @@ -22,6 +22,13 @@ function setup() { stopButton.mousePressed(function() { fab.stopPrint(); // stop streaming the commands to printer }); + + let exportButton = createButton('export!'); + exportButton.position(20, 140); + exportButton.mousePressed(function() { + fab.exportGcode(); // export gcode to a file. + }); + } diff --git a/examples/template/sketch.js b/examples/template/sketch.js index 548a902..040631d 100644 --- a/examples/template/sketch.js +++ b/examples/template/sketch.js @@ -22,6 +22,12 @@ function setup() { fab.stopPrint(); // stop streaming the commands to printer. }); + let exportButton = createButton('export!'); + exportButton.position(20, 140); + exportButton.mousePressed(function() { + fab.exportGcode(); // export gcode to a file. + }); + } diff --git a/examples/vase/sketch.js b/examples/vase/sketch.js index 17cf26f..daf95ee 100644 --- a/examples/vase/sketch.js +++ b/examples/vase/sketch.js @@ -6,17 +6,29 @@ function setup() { fab = createFab(); // add a buttons to connect to the printer & to print! - connectButton = createButton('connect!'); + let connectButton = createButton('connect!'); connectButton.position(20, 20); connectButton.mousePressed(function() { fab.serial.requestPort(); }); - printButton = createButton('print!'); + let printButton = createButton('print!'); printButton.position(20, 60); printButton.mousePressed(function() { fab.print(); }); + + let stopButton = createButton('stop!'); + stopButton.position(20, 100); + stopButton.mousePressed(function() { + fab.stopPrint(); // stop streaming the commands to printer. + }); + + let exportButton = createButton('export!'); + exportButton.position(20, 140); + exportButton.mousePressed(function() { + fab.exportGcode(); // export gcode to a file. + }); } function draw() { diff --git a/lib/p5.fab.js b/lib/p5.fab.js index 2ffab88..f935c0e 100644 --- a/lib/p5.fab.js +++ b/lib/p5.fab.js @@ -189,22 +189,30 @@ class Fab { this.serialResp = ""; this.callbacks = {}; - this.serial.on("portavailable", function () { - _fab.serial.open({ baudRate: _fab.baudRate }); + // Convert event handlers to async/await + this.serial.on("portavailable", async () => { + try { + await this.serial.open({ baudRate: this.baudRate }); + console.log("Port opened successfully"); + } catch (err) { + console.error("Failed to open port:", err); + } }); - this.serial.on("requesterror", function () { - console.log("error!"); + this.serial.on("requesterror", (err) => { + console.error("Error requesting port:", err); }); - this.serial.on("data", this.onData); + // Bind onData to preserve 'this' context + this.serial.on("data", () => this.onData()); - this.serial.on("open", function () { - console.log("port open"); + this.serial.on("open", () => { + console.log("Port open"); + this.emit("connected", this); }); if (config.autoConnect) { - this.serial.getPorts(); + this.connectPrinter(); } this.on("ok", this.serial_ok); @@ -262,6 +270,29 @@ class Fab { this.midiMode = false; this.midiSetup = null; this.midiDraw = null; + + // Add response types + this.RESPONSE_TYPES = { + OK: 'ok', + POSITION: 'Count', + AUTOHOME: 'Autohome complete', + PRINT_FINISHED: 'Print Finished' + }; + + // Add buffer management + this.messageBuffer = ''; + this.lastMessageTime = Date.now(); + } + + // Add new async connect method + async connectPrinter() { + try { + await this.serial.requestPort(); + // Port selection will trigger the "portavailable" event handler + } catch (err) { + console.error("Failed to request port:", err); + this.emit("error", err); + } } getStackTrace() { @@ -286,154 +317,44 @@ class Fab { console.log("print in progress, cant start a new print"); return; } - if (!this.isPrinting && _syncVizStream) { - this.commandStream = this.commands; - _syncVizStream = false; - } - // send first commands + // Create fresh copy of commands for printing + this.commandStream = [...this.commands]; + this.commands = []; // Clear commands after copying + + // Send first command if we have any if (this.commandStream.length > 0) { this.isPrinting = true; this.serial.write(this.commandStream[0] + "\n"); - console.log("starting print"); + console.log("sending:", this.commandStream[0]); this.commandStream.shift(); - } else { - console.log("print finished!"); - this.isPrinting = false; - } - - if (this.fabscribe) { - this.startTime = Date.now(); - - this.mediaRecorder = new MediaRecorder(this.videoStream, { mimeType: 'video/webm' }); - - this.mediaRecorder.addEventListener('dataavailable', function (e) { - _fab.blobsRecorded.push(e.data); - }); - - this.mediaRecorder.addEventListener('stop', function () { - // create local object URL from the recorded video blobs to download - let videoLocal = URL.createObjectURL(new Blob(_fab.blobsRecorded, { type: 'video/webm' })); - const videoLink = document.createElement("a"); - videoLink.href = videoLocal; - videoLink.download = 'test.webm'; - videoLink.click(); - }); - - // start recording with each recorded blob having 1 second video - fab.mediaRecorder.start(1000); } } - printStream() { - if (this.commandStream.length > 0) { - this.isPrinting = true; - let commandToSend = this.commandStream[0]; - if (this.midiMode) { - commandToSend = this.updateWithMidiValues(this.commandStream[0]); - } - this.serial.write(commandToSend + "\n"); - if (this.fabscribe) { - this.fabscription(commandToSend); + processMessage(message) { + // Log all messages for debugging + console.log('Received:', message); + + if (message.includes('ok')) { + // Continue printing if there are more commands + if (this.commandStream.length > 0) { + this.serial.write(this.commandStream[0] + "\n"); + console.log("sending:", this.commandStream[0]); + this.commandStream.shift(); + } else { + console.log("print finished!"); + this.isPrinting = false; } - this.commandStream.shift(); - } else { - console.log("print finished!"); - this.isPrinting = false; - } - } - - fabscription(commandToSend) { - this.sentCommands.push(commandToSend); // add this sent commands - - if (commandToSend != "M114 R") { - // this is to remove position logs from the gcode - // Might need to instead include & look for a special character - // in case the operator actually sends and M114 R - // but for now, its only used for transcription - this.sentCommandsFiltered.push(commandToSend); // add this sent commands } - // Confirm when buffer is first filled - if (this.autoHomeComplete && !this.bufferFilled) { - const bufferedCommands = ["G0", "G1", "G2", "G3", "G90", "G91", "M82", "M83"]; // TODO: Check this list for other firmware - let cmdType = this.commandStream[0].split(' ')[0]; - if (bufferedCommands.includes(cmdType)) { - let cmdSizeBytes = (new TextEncoder().encode(this.commandStream[0] + "\n")).length; - this.bufferFillSize = this.bufferFillSize + cmdSizeBytes; - console.log('buffer size is:', this.bufferFillSize); - - // get the next buffered command and see if that will put us over the buffer size - let checkFullBuffer = false; - let cmdCheckIndex = 1; - while (!checkFullBuffer) { - let nextCmd = this.commandStream[cmdCheckIndex]; - let nextCmdType = this.commandStream[0].split(' ')[0]; - if (bufferedCommands.includes(nextCmdType)) { - checkFullBuffer = true; - let nextCmdSizeBytes = (new TextEncoder().encode(nextCmd + "\n")).length; - let nextBufferSize = this.bufferFillSize + nextCmdSizeBytes; - if (nextBufferSize > this.bufferSize) { - console.log('command full after the next command added!'); - this.bufferFilled = true; - console.log('it took this many commands to fill the buffer:', this.numCommandsToFillBuffer); - - // pad the end of the print with the same # of commands for transcription to persist through end of print - for (let i = 0; i <= this.numCommandsToFillBuffer + 1; i++) { - this.commandStream.push(`G1 X100 Y100 Z${100 + i} ;;; cut this line`); // change this to something more robust? - this.commandStream.push("M118 Print Finished"); // for transcription - } - } - else { - cmdCheckIndex += 1; - } - } - } - } - this.numCommandsToFillBuffer += 1; + // Handle position reporting + if (message.includes('Count')) { + this.reportedPos = message.split('Count')[0].trim(); } } - updateWithMidiValues(cmd) { - // TODO: should first remove any comments - let moveCommands = ['G0', 'G1', 'G2', 'G3']; - let splitCmd = cmd.split(' '); - let code = splitCmd[0]; - - // record all midi values - // probably want to filter this by tag, to only show the value when the value is being applied to the print - // currently manually grabbing each value, should dynamically get all relevant knobs from midi.js - var currentTime = Date.now() - _fab.startTime; - - if (this.fabscribe) { - // grab all the user specified props for the midi controller and record them - for (const property in _midiController) { - if (property == 'debug') { - continue; - } - this.midiRecording[property] = this.midiRecording[property] || []; - this.midiRecording[property].push([currentTime, _midiController[property]]); - } - } - - - if (moveCommands.indexOf(code) > -1) { - // update the 'realtime' positions - // This is technically a bit in front of real position depending on buffer size - // consider using M114 R? - let moveCommand = new LinearMove(cmd); - - if (moveCommand.x) { this.realtimePosition.x = moveCommand.x }; - if (moveCommand.y) { this.realtimePosition.y = moveCommand.y }; - if (moveCommand.z) { this.realtimePosition.z = moveCommand.z }; - - if (this.midiDraw) { - moveCommand = this.midiDraw(moveCommand); // run the midiDraw function - cmd = moveCommand.toString(); - } - } - - return cmd + getPos() { + this.add("M114"); } on(event, cb) { @@ -453,56 +374,101 @@ class Fab { } onData() { - if (_fab.isPrinting) { - _fab.serialResp += _fab.serial.readString(); - - if (_fab.serialResp.slice(-1) == "\n") { - if (_fab.serialResp.search("ok") > -1) { - _fab.emit("ok", _fab); - let currentTime = Date.now() - _fab.startTime; - - // get position, if we didn't just do that, autohoming is complete, and the buffer is filled - // (if things are exectuted out of order, does this mess up?) - if (_fab.fabscribe) { - if (_fab.autoHomeComplete && _fab.bufferFilled) { - if (_fab.sentCommands[_fab.sentCommands.length - 1] != "M114 R") { - _fab.commandStream.unshift("M114 R"); - } - } - } - } + try { + // Read new data + const newData = this.serial.readString(); + if (!newData) return; - if (_fab.serialResp.search(" Count ") > -1) { - _fab.reportedPos = _fab.serialResp.split(" Count ")[0].trim(); - if (_fab.autoHomeComplete && _fab.bufferFilled) { - var logEntry = [Date.now() - _fab.startTime, _fab.reportedPos]; - _fab.log.push(logEntry); - } - } + // Update buffer and timestamp + this.messageBuffer += newData; + this.lastMessageTime = Date.now(); - if (_fab.serialResp.search("Autohome complete") > -1) { - // for transcription: toggle to start sending M114 R commands - _fab.autoHomeComplete = true; - var logEntry = [Date.now() - _fab.startTime, "AUTOHOMED"]; - _fab.log.push(logEntry); - //this is the keyword hosts like e.g. pronterface search for M114 respos (https://github.com/kliment/Printrun/issues/1103) - _fab.reportedPos = _fab.serialResp.split(" Count ")[0].trim(); - } + // Process messages + this.processMessageBuffer(); - if (_fab.serialResp.search("Print Finished") > -1) { - if (_fab.fabscribe) { - if (!_fab.hasDownloadedLog) { - _fab.mediaRecorder.stop(); - _fab.downloadFabscriptionLog(); - _fab.hasDownloadedLog = true; - } - } + } catch (error) { + console.error('Error in onData:', error); + this.emit('error', error); + } + } + + processMessageBuffer() { + // Split buffer into complete messages + const messages = this.messageBuffer.split('\n'); + + // Keep last incomplete message in buffer + this.messageBuffer = messages.pop() || ''; + + // Process each complete message + messages.forEach(message => { + if (message.trim()) { + this.processMessage(message.trim()); + } + }); + } + + getMessageType(message) { + if (message.includes(this.RESPONSE_TYPES.OK)) return 'ok'; + if (message.includes(this.RESPONSE_TYPES.POSITION)) return 'position'; + if (message.includes(this.RESPONSE_TYPES.AUTOHOME)) return 'autohome'; + if (message.includes(this.RESPONSE_TYPES.PRINT_FINISHED)) return 'print_finished'; + return 'unknown'; + } + + handleOkResponse() { + this.emit('ok', this); + + if (this.isPrinting) { + // Handle position query for fabscribe + if (this.fabscribe && this.autoHomeComplete && this.bufferFilled) { + if (this.sentCommands[this.sentCommands.length - 1] !== "M114 R") { + this.commandStream.unshift("M114 R"); } - _fab.serialResp = ""; } } } + handlePositionResponse(message) { + const position = message.split(" Count ")[0].trim(); + this.reportedPos = position; + + if (this.autoHomeComplete && this.bufferFilled) { + const logEntry = [Date.now() - this.startTime, position]; + this.log.push(logEntry); + } + } + + handleAutohomeResponse() { + this.autoHomeComplete = true; + if (this.fabscribe) { + const logEntry = [Date.now() - this.startTime, "AUTOHOMED"]; + this.log.push(logEntry); + } + } + + handlePrintFinished() { + if (this.fabscribe && !this.hasDownloadedLog) { + this.mediaRecorder.stop(); + this.downloadFabscriptionLog(); + this.hasDownloadedLog = true; + } + this.resetPrinterState(); + } + + handleUnknownResponse(message) { + console.log('Unknown printer response:', message); + } + + // Add message timeout checking + checkMessageTimeout() { + const TIMEOUT_MS = 5000; // 5 seconds + if (this.messageBuffer && (Date.now() - this.lastMessageTime > TIMEOUT_MS)) { + console.warn('Message timeout - clearing buffer'); + this.messageBuffer = ''; + this.emit('warning', 'Message timeout occurred'); + } + } + parseGcode() { this.vertices = []; _fab.commands.forEach((cmd) => { @@ -575,6 +541,22 @@ class Fab { }); } + exportGcode(fileName = new Date().toISOString().slice(0, 19).replace(/:/g, "")) { + let gcodeText = ""; + fab.commands.forEach(command => { + gcodeText += command + "\n"; + }); + + let element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(gcodeText)); + element.setAttribute('download', `${fileName}.gcode`); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + + } + render() { if (this.coordinateSystem == "delta") { this.drawDeltaPrinter(); @@ -772,6 +754,8 @@ class Fab { this.commandStream.unshift(cmd); } + + configure(config) { this.coordinateSystem = config.coordinateSystem; this.radius = config.radius; @@ -796,15 +780,10 @@ class Fab { } stopPrint() { - this.commandStream = []; // clear commands - this.isPrinting = false; - // this.serial.close(); - // this.serial.getPorts(); - fabDraw(); - + this.resetPrinterState(); if (this.fabscribe) { - fab.mediaRecorder.stop(); - fab.downloadFabscriptionLog(); + this.mediaRecorder.stop(); + this.downloadFabscriptionLog(); } } @@ -845,17 +824,25 @@ class Fab { this.add(cmd); } + finishPrint() { + var finishCommands = [ + "G1 Z79 F600 ; Move print head further up", + "G1 Z150 F600 ; Move print head further up", + "M140 S0 ; turn off heatbed", + "M104 S0 ; turn off temperature", + "M107 ; turn off fan", + "M84 X Y E ; disable motors"]; + + finishCommands.forEach(cmd => { + this.add(cmd); + }); + } + waitCommand() { var cmd = "M400"; this.add(cmd); } - getPos() { - var cmd = "M114_DETAIL"; - var cmd = "M114 D"; - this.add(cmd); - } - setPos() { var cmd = `G92 X${this.asyncPosition.x} Y${this.asyncPosition.y} Z${this.asyncPosition.z} E${this.asyncPosition.e}`; } @@ -1043,6 +1030,14 @@ class Fab { // fab.midiRecording.extrusionMultiplier.forEach((extrusionEntry) => midiWriter.print(extrusionEntry)); // midiWriter.close(); } + + // Add method to properly reset printer state + resetPrinterState() { + this.isPrinting = false; + this.commandStream = []; + this.serialResp = ""; + console.log("Printer state reset"); + } } function windowResized() {