diff --git a/package-lock.json b/package-lock.json index 8e6597c..21fcd11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,8 +54,8 @@ "react-notification-system-redux": "1.2.0", "react-redux": "5.0.6", "react-resize-detector": "1.1.0", - "react-svg-inline": "2.0.1", - "redux-form": "7.2.0", + "react-svg-inline": "2.1.0", + "redux-form": "7.2.1", "redux-undo": "1.0.0-beta9-9-7", "reselect": "3.0.1", "semver": "5.4.1", @@ -6942,9 +6942,9 @@ } }, "react-svg-inline": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/react-svg-inline/-/react-svg-inline-2.0.1.tgz", - "integrity": "sha512-9YVqJ80g1gPWAvD9CS/z4cKPD45ZSMjjzwxFAmQJiMEoAo1Ajhz92WirXag3ftltDN5lPNkVWx/KOnEWB/PaMQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-svg-inline/-/react-svg-inline-2.1.0.tgz", + "integrity": "sha512-GzRID5IcEQ8dnnaUtTb9MDTAbhuaOiVKKAVLgrCNuehHsg3DuZbe82bjc9JhmPv0zsDWhDrJwzADNgzEvE6VeQ==", "requires": { "classnames": "2.2.5", "prop-types": "15.6.0" @@ -7058,9 +7058,9 @@ } }, "redux-form": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/redux-form/-/redux-form-7.2.0.tgz", - "integrity": "sha512-qbgeI19drwnm9FeGAotDA1vsZO8q94XF7IxPDuJmSXxDYX2rqzhND6NROahCBJfBK5xM1cchvmgscO2rry1EEw==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/redux-form/-/redux-form-7.2.1.tgz", + "integrity": "sha512-KWV+rq+L1QGoRSKoJXbGS8Mw2q4ta5FVyGxW5ZYnAEjXZAukvUCkqDUzobBmOqiRHvrZ3/ssEA7kJFdu7rV8+w==", "requires": { "deep-equal": "1.0.1", "es6-error": "4.1.1", @@ -8271,6 +8271,11 @@ "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=" }, + "validate-ip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/validate-ip/-/validate-ip-1.0.1.tgz", + "integrity": "sha1-615PY+HRq8buRuGK4gaXv1vtBto=" + }, "validate-npm-package-license": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", diff --git a/package.json b/package.json index 8b655e4..473f113 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react-resize-detector": "^1.1.0", "shortid": "^2.2.8", "three": "^0.88.0", + "validate-ip": "^1.0.1", "webpack-bundle-analyzer": "^2.9.2" }, "devDependencies": { diff --git a/src/interface/Settings.js b/src/interface/Settings.js index 5c7fb5e..9a5276c 100644 --- a/src/interface/Settings.js +++ b/src/interface/Settings.js @@ -18,6 +18,7 @@ import materialSettings from '../settings/material.yml'; import qualitySettings from '../settings/quality.yml'; import update from 'react-addons-update'; import SettingsIcon from 'material-ui-icons/Settings'; +import validateIp from 'validate-ip'; const styles = { textFieldRow: { @@ -79,6 +80,7 @@ class Settings extends React.Component { open: false, name: '', printer: '', + ip: '', error: null }, managePrinter: { @@ -102,25 +104,22 @@ class Settings extends React.Component { let state = _.cloneDeep(this.state); - const removeAddPrinterError = () => { - state = update(state, { addPrinter: { error: { $set: null } } }); - }; - switch (fieldName) { case 'managePrinter.printer': case 'managePrinter.name': + case 'managePrinter.ip': state = _.set(state, fieldName, value); + state = update(state, { managePrinter: { error: { $set: null } } }); break; case 'addPrinter.printer': - state = update(state, { addPrinter: { printer: { $set: value } } }); - state = update(state, { addPrinter: { name: { $set: printerSettings[value].title } } }); - removeAddPrinterError(); - break; - case 'addPrinter.name': - state = update(state, { addPrinter: { name: { $set: value } } }); - removeAddPrinterError(); + case 'addPrinter.ip': + state = _.set(state, fieldName, value); + if (fieldName === 'addPrinter.printer') { + state = update(state, { addPrinter: { name: { $set: printerSettings[value].title } } }); + } + state = update(state, { addPrinter: { error: { $set: null } } }); break; case 'activePrinter': @@ -212,12 +211,13 @@ class Settings extends React.Component { constructSettings(localStorage) { if (!localStorage.active) return defaultSettings; - const { printer, material, quality, advanced } = localStorage.printers[localStorage.active].settings; + const { ip, settings: { printer, material, quality, advanced } } = localStorage.printers[localStorage.active]; let settings = { ...defaultSettings, printer, material, - quality + quality, + ip }; settings = _.merge({}, settings, printerSettings[printer]); @@ -233,10 +233,14 @@ class Settings extends React.Component { } addPrinter = () => { - const { name, printer } = this.state.addPrinter; + const { name, printer, ip } = this.state.addPrinter; if (!name || !printer) { - this.setState({ addPrinter: { ...this.state.addPrinter, error: 'Please enter a name and printer' } }); + this.setState(update(this.state, { addPrinter: { error: { $set: 'Please enter a name and printer' } } })); + return; + } + if (printer === 'doodle3d_printer' && !validateIp(ip)) { + this.setState(update(this.state, { addPrinter: { error: { $set: 'Please enter a valid IP adress' } } })); return; } @@ -245,7 +249,7 @@ class Settings extends React.Component { active: id, printers: { ...this.state.localStorage.printers, - [id]: { name, settings: { printer, material: 'pla', quality: 'medium', advanced: {} } } + [id]: { name, ip, settings: { printer, material: 'pla', quality: 'medium', advanced: {} } } } }; this.setState({ localStorage }); @@ -258,11 +262,22 @@ class Settings extends React.Component { }; editPrinter = () => { - const { localStorage: { active, printers }, managePrinter: { printer, name } } = this.state; + const { localStorage: { active, printers }, managePrinter: { printer, name, ip } } = this.state; + + if (!name) { + this.setState(update(this.state, { managePrinter: { error: { $set: 'Please enter a name' } } })); + return; + } + if (printer === 'doodle3d_printer' && !validateIp(ip)) { + this.setState(update(this.state, { managePrinter: { error: { $set: 'Please enter a valid IP adress' } } })); + return; + } + const localStorage = update(this.state.localStorage, { printers: { [active]: { name: { $set: name }, + ip: { $set: ip }, settings: { printer: { $set: printer } } @@ -296,7 +311,7 @@ class Settings extends React.Component { closeAddPrinterDialog = () => this.setAddPrinterDialog(false); openAddPrinterDialog = () => this.setAddPrinterDialog(true); - setAddPrinterDialog = (open) => this.setState({ addPrinter: { name: '', printer: '', error: null, open } }); + setAddPrinterDialog = (open) => this.setState({ addPrinter: { ip: '', name: '', printer: '', error: null, open } }); closeManagePrinterDialog = () => this.setManagePrinterDialog(false); openManagePrinterDialog = () => this.setManagePrinterDialog(true); @@ -307,7 +322,9 @@ class Settings extends React.Component { managePrinter: { open, name: printers[active].name, - printer: printers[active].settings.printer + ip: printers[active].ip, + printer: printers[active].settings.printer, + error: null } }); } @@ -432,6 +449,7 @@ class Settings extends React.Component { ))} + {(addPrinter.printer === 'doodle3d_printer') && } {addPrinter.error &&

{addPrinter.error}

} + {(managePrinter.printer === 'doodle3d_printer') && } + {managePrinter.error &&

{managePrinter.error}

}
); diff --git a/src/interface/index.js b/src/interface/index.js index 47f8bff..7f8f9b2 100644 --- a/src/interface/index.js +++ b/src/interface/index.js @@ -209,6 +209,13 @@ class Interface extends React.Component { const { name } = this.props; if (isSlicing) return; + if (!settings) { + this.setState({ error: 'please select a printer first' }); + } + if (target === 'WIFI' && !settings.ip) { + this.setState({ error: 'please connect to a WiFi enabled printer' }); + return; + } if (!mesh) { this.setState({ error: 'there is no file to slice' }); return; @@ -311,7 +318,7 @@ class Interface extends React.Component { render() { const { classes, onCancel } = this.props; - const { isSlicing, progress, showFullScreen, error, objectDimensions, openUrlDialog } = this.state; + const { isSlicing, progress, showFullScreen, error, objectDimensions, openUrlDialog, settings } = this.state; const style = { ...(showFullScreen ? {} : { maxWidth: 'inherit', width: '100%', height: '100%' }) }; @@ -349,7 +356,7 @@ class Interface extends React.Component { onRequestClose={this.closePopover} > - this.slice('WIFI')} /> + this.slice('WIFI')} /> this.slice('DOWNLOAD')} /> diff --git a/src/interface/utils.js b/src/interface/utils.js index 3dd601e..8f9b381 100644 --- a/src/interface/utils.js +++ b/src/interface/utils.js @@ -106,9 +106,10 @@ export function fetchProgress(url, data = {}, onProgress) { if (xhr.upload && onProgress) xhr.upload.onprogress = onProgress; if (xhr.responseType) xhr.responseType = 'blob'; - request.headers.forEach((value, name) => { - xhr.setRequestHeader(name, value) - }); + // Malyan printer doesn't like headers... + // request.headers.forEach((value, name) => { + // xhr.setRequestHeader(name, value) + // }); xhr.send(data.body); }); @@ -118,8 +119,6 @@ const GCODE_SERVER_URL = 'https://gcodeserver.doodle3d.com'; const CONNECT_URL = 'http://connect.doodle3d.com/'; export async function slice(target, name, mesh, settings, updateProgress) { - if (!settings) throw { message: 'please select a printer first', code: 0 }; - let steps; let currentStep = 0; switch (target) { @@ -127,6 +126,7 @@ export async function slice(target, name, mesh, settings, updateProgress) { steps = 1; break; case 'WIFI': + case 'DOODLE3D-WIFI-BOX': steps = 2; break; default: @@ -151,12 +151,30 @@ export async function slice(target, name, mesh, settings, updateProgress) { switch (target) { case 'DOWNLOAD': { - const blob = new File([gcode], `${name}.gcode`, { type: 'text/plain;charset=utf-8' }); + const blob = new File([gcode], `${name}.gcode`, { type: 'text/plain' }); fileSaver.saveAs(blob); break; } case 'WIFI': { + const body = new FormData(); + const file = new File([gcode], 'doodle.gcode', { type: 'plain/text' }); + body.append('file', file); + + await fetchProgress(`http://${settings.ip}/set?code=M563 S4`, { method: 'GET' }); + await fetchProgress(`http://${settings.ip}/upload`, { method: 'POST', body }, (progress) => { + updateProgress({ + action: 'Uploading', + percentage: currentStep / steps + progress.loaded / progress.total / steps + }); + }); + currentStep ++; + await fetchProgress(`http://${settings.ip}/set?code=M566 ${name}.gcode`, { method: 'GET' }); + await fetchProgress(`http://${settings.ip}/set?code=M565`, { method: 'GET' }); + break; + } + + case 'DOODLE3D-WIFI-BOX': { // upload G-code file to AWS S3 const { data: { reservation, id } } = await fetch(`${GCODE_SERVER_URL}/upload`, { method: 'POST' }) .then(response => response.json()); @@ -176,10 +194,10 @@ export async function slice(target, name, mesh, settings, updateProgress) { }).trim()}\n${gcode}`; body.append('file', file); - await fetchProgress(reservation.url, { method: 'POST', body }, (progess) => { + await fetchProgress(reservation.url, { method: 'POST', body }, (progress) => { updateProgress({ action: 'Uploading', - percentage: currentStep / steps + progess.loaded / progess.total / steps + percentage: currentStep / steps + progress.loaded / progress.total / steps }); }); currentStep ++; diff --git a/test.js b/test.js new file mode 100644 index 0000000..d448d41 --- /dev/null +++ b/test.js @@ -0,0 +1,122 @@ +import 'babel-polyfill' +import React from 'react'; +import { render } from 'react-dom'; +import injectTapEventPlugin from 'react-tap-event-plugin'; +// import './fetch.js'; + +injectTapEventPlugin(); + +const IP = 'http://10.0.0.109'; +const DEFAULT_GCODE = `; Generated with Doodle3D Slicer V0.0.18 +G28 +G1 X30 Y30 +G1 X90 Y30 +G1 X30 Y30 +G1 X90 Y30 +G1 X30 Y30 +G1 X90 Y30 +G1 X30 Y30 +G1 X90 Y30 +G1 X30 Y30 +G1 X90 Y30 +G1 X30 Y30 +G1 X90 Y30 +; test +`; + +const CIRCLE = `; Generated with Doodle3D Slicer V0.0.18 +G28 +G1 X50 Y70 +G1 X56.180339887498945 Y69.02113032590307 +G1 X61.75570504584947 Y66.18033988749895 +G1 X66.18033988749895 Y61.75570504584947 +G1 X69.02113032590307 Y56.180339887498945 +G1 X70 Y50 +G1 X69.02113032590307 Y43.819660112501055 +G1 X66.18033988749895 Y38.24429495415054 +G1 X61.75570504584947 Y33.819660112501055 +G1 X56.18033988749895 Y30.97886967409693 +G1 X50 Y30 +G1 X43.819660112501055 Y30.978869674096927 +G1 X38.24429495415054 Y33.819660112501055 +G1 X33.819660112501055 Y38.24429495415053 +G1 X30.97886967409693 Y43.81966011250105 +G1 X30 Y49.99999999999999 +G1 X30.978869674096927 Y56.180339887498945 +G1 X33.81966011250105 Y61.75570504584946 +G1 X38.24429495415053 Y66.18033988749895 +G1 X43.81966011250105 Y69.02113032590307 +; test +`; + +// export function fetch(url, data = {}, onProgress) { +// return new Promise((resolve, reject) => { +// const request = new Request(url, data); +// const xhr = new XMLHttpRequest(); +// +// xhr.onload = () => { +// const { status, statusText, responseURL: url } = xhr; +// resolve(new Response(xhr.response, { status, statusText, url })); +// } +// xhr.onerror = () => reject(new TypeError('Network request failed')); +// xhr.ontimeout = () => reject(new TypeError('Network request failed')); +// +// xhr.open(request.method, url); +// +// if (request.credentials === 'include') { +// xhr.withCredentials = true +// } else if (request.credentials === 'omit') { +// xhr.withCredentials = false +// } +// if (xhr.upload && onProgress) xhr.upload.onprogress = onProgress; +// if (xhr.responseType) xhr.responseType = 'blob'; +// +// request.headers.forEach((value, name) => { +// xhr.setRequestHeader(name, value) +// }); +// +// xhr.send(data.body); +// }); +// } + +class Print extends React.Component { + home = () => fetch(`${IP}/set?code=G28`, { method: 'GET', mode: 'no-cors' }); + status = () => fetch(`${IP}/inquiry`, { method: 'GET', mode: 'no-cors' }) + .then(response => response.text()) + .then(result => console.log('result: ', result)); + start = () => fetch(`${IP}/set?code=M565`, { method: 'GET', mode: 'no-cors' }); + stop = () => fetch(`${IP}/set?cmd={P:X}`, { method: 'GET', mode: 'no-cors' }); + upload = async () => { + const gcode = this.refs.gcode.value; + + const headers = new Headers(); + headers.append('Content-Disposition', 'form-data; name="file"; filename="doodle.gcode"'); + headers.append('Content-Type', 'application/octet-stream'); + headers.append('Accept', 'application/json'); + + const body = new FormData(); + const file = new File([gcode], 'doodle.gcode', { type: 'application/octet-stream' }); + body.append('file', file); + + const result = await fetch(`${IP}/upload`, { method: 'POST', mode: 'no-cors', headers, body }); + }; + + render() { + return ( + + + + + +
+