diff --git a/packages/xod-client-electron/package.json b/packages/xod-client-electron/package.json index c2d72ede..dfc5bb04 100644 --- a/packages/xod-client-electron/package.json +++ b/packages/xod-client-electron/package.json @@ -15,6 +15,7 @@ "dependencies": { "babel-polyfill": "^6.16.0", "classnames": "^2.2.5", + "electron-settings": "^3.0.14", "ramda": "^0.23.0", "rc-progress": "^2.0.6", "react": "^15.4.2", @@ -28,8 +29,10 @@ "redux-thunk": "^2.1.0", "serialport": "^4.0.7", "xod-arduino": "^0.0.1", + "xod-arduino-builder": "^0.0.1", "xod-client": "^0.0.1", "xod-espruino-upload": "^0.0.1", + "xod-func-tools": "^0.0.1", "xod-fs": "^0.0.1", "xod-js": "^0.0.1", "xod-project": "^0.0.1", diff --git a/packages/xod-client-electron/src/app/main.js b/packages/xod-client-electron/src/app/main.js index e57a0eb0..b2e2b8a6 100644 --- a/packages/xod-client-electron/src/app/main.js +++ b/packages/xod-client-electron/src/app/main.js @@ -1,10 +1,16 @@ +import R from 'ramda'; import { app, ipcMain, BrowserWindow, } from 'electron'; import devtron from 'devtron'; +import settings from 'electron-settings'; +import { resolvePath } from 'xod-fs'; import { saveProject, loadProjectList, loadProject, changeWorkspace, checkWorkspace } from './remoteActions'; +import { checkArduinoIde, installPav, findPort, doTranspileForArduino, uploadToArduino } from './uploadActions'; + +app.setName('xod'); // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. @@ -35,6 +41,21 @@ function createWindow() { }); } +const setDefaultSettings = () => { + if (R.isEmpty(settings.getAll())) { + settings.setAll({ + arduino: { + paths: { + // TODO: Add paths for other OS + ide: '/Applications/Arduino.app/Contents/MacOS/Arduino', + packages: resolvePath('~/Library/Arduino15/packages/'), + }, + pavs: [], + }, + }); + } +}; + const subscribeRemoteAction = (processName, remoteAction) => { ipcMain.on(processName, (event, opts) => { event.sender.send(`${processName}:process`); @@ -46,13 +67,37 @@ const subscribeRemoteAction = (processName, remoteAction) => { const onReady = () => { devtron.install(); + setDefaultSettings(); + // TODO: Replace actionTypes with constants (after removing webpack from this package) subscribeRemoteAction('saveProject', saveProject); subscribeRemoteAction('loadProjectList', loadProjectList); subscribeRemoteAction('loadProject', loadProject); subscribeRemoteAction('checkWorkspace', checkWorkspace); subscribeRemoteAction('changeWorkspace', changeWorkspace); + ipcMain.on('UPLOAD_TO_ARDUINO', + (event, payload) => { + let code; + let port; + + let percentage = 0; + function reply(data) { + percentage += data.percentage; + event.sender.send('UPLOAD_TO_ARDUINO', R.merge(data, { percentage })); + } + + doTranspileForArduino(payload, reply) + .then((transpiledCode) => { code = transpiledCode; }) + .then(() => findPort(payload.pab, reply)) + .then((foundPort) => { port = foundPort; }) + .then(() => checkArduinoIde(settings.get('arduino.paths'), reply)) + .then(() => installPav(payload.pab, reply)) + .then(() => uploadToArduino(payload.pab, port, code, reply)) + .catch(reply); + } + ); + createWindow(); }; diff --git a/packages/xod-client-electron/src/app/uploadActions.js b/packages/xod-client-electron/src/app/uploadActions.js new file mode 100644 index 00000000..dbcacf63 --- /dev/null +++ b/packages/xod-client-electron/src/app/uploadActions.js @@ -0,0 +1,123 @@ +import R from 'ramda'; +import fs from 'fs'; +import { writeFile, isDirectoryExists, isFileExists } from 'xod-fs'; +import { foldEither } from 'xod-func-tools'; +import { transpileForArduino } from 'xod-arduino'; +import * as xab from 'xod-arduino-builder'; + +// TODO: Replace types with constants (after removing webpack from this package) +// TODO: Move messages to somewhere + +const throwError = R.curry((defaultObject, err) => Promise.reject( + R.merge(defaultObject, { code: 1, message: err.message }) +)); + +export const checkArduinoIde = ({ ide, packages }, success) => { + const ideExists = isFileExists(ide); + const pkgExists = isDirectoryExists(packages); + const result = { + code: (ideExists && pkgExists) ? 0 : 1, + type: 'IDE', + message: 'Arduino IDE has been found. Checking for toolchain...', + percentage: 5, + }; + + if (!ideExists) { result.message = 'Arduino IDE not found.'; } + if (ideExists && !pkgExists) { result.message = 'Package folder not found.'; } + + return xab.setArduinoIdePathPackages(packages) + .then(() => xab.setArduinoIdePathExecutable(ide)) + .then(() => success(result)) + .catch(throwError(result)); +}; + +const getPAV = pab => R.composeP( + R.last, + R.prop(`${pab.package}:${pab.architecture}`), + xab.listPAVs +)(); + +export const installPav = (pab, success) => { + const result = { + code: 0, + type: 'TOOLCHAIN', + message: 'Toolchain is installed. Uploading...', + percentage: 20, + }; + + return getPAV(pab) + .then(xab.installPAV) + .then(() => success(result)) + .catch(throwError(result)); +}; + +export const findPort = (pab, success) => { + const result = { + code: 0, + type: 'PORT', + message: 'Port with connected Arduino was found. Preparing toolchain...', + percentage: 5, + }; + + return xab.listPorts() + .then(ports => R.compose( + R.ifElse( + R.isNil, + () => Promise.reject(new Error('Could not find Arduino device on opened ports.')), + port => Promise.resolve(port) + ), + R.propOr(null, 'comName'), + R.find( + R.propEq('vendorId', '0x2341') // TODO: Replace it with normal find function + ) + )(ports)) + .then((port) => { success(result); return port; }) + .catch(throwError(result)); +}; + +export const doTranspileForArduino = ({ pab, project, patchId }, success) => { + const result = { + code: 0, + type: 'TRANSPILING', + message: 'Code has been transpiled. Prepare to upload...', + percentage: 15, + }; + + return Promise.resolve(project) + .then(v2 => transpileForArduino(v2, patchId)) + .then(foldEither( + err => Promise.reject(new Error(err)), + code => Promise.resolve(code) + )) + .then((code) => { success(result); return code; }) + .catch(throwError(result)); +}; + +export const uploadToArduino = (pab, port, code, success) => { + // TODO: Replace path to temp directory (with workspace?) + const tmpPath = `${__dirname}/uploadCode.cpp`; + const result = { + code: 0, + type: 'UPLOAD', + message: 'Code has been successfully uploaded.', + percentage: 55, + }; + const clearTmp = () => fs.unlinkSync(tmpPath); + + return writeFile(tmpPath, code) + .then(({ path }) => xab.upload(pab, port, path)) + .then((response) => { result.message = response; }) + .catch((errorMessage) => { + clearTmp(); + return Promise.reject(new Error(`Can't build or upload project to Arduino.\n${errorMessage}`)); + }) + .then(() => success(result)) + .then(() => clearTmp()) + .catch(throwError(result)); +}; + +export default { + checkArduinoIde, + installPav, + uploadToArduino, +}; diff --git a/packages/xod-client-electron/src/upload/actionTypes.js b/packages/xod-client-electron/src/upload/actionTypes.js index e9a4715d..528decd5 100644 --- a/packages/xod-client-electron/src/upload/actionTypes.js +++ b/packages/xod-client-electron/src/upload/actionTypes.js @@ -1,5 +1,7 @@ export const UPLOAD = 'UPLOAD'; +export const UPLOAD_TO_ARDUINO = 'UPLOAD_TO_ARDUINO'; export default { UPLOAD, + UPLOAD_TO_ARDUINO, }; diff --git a/packages/xod-client-electron/src/upload/actions.js b/packages/xod-client-electron/src/upload/actions.js index 18877e00..75ce41b9 100644 --- a/packages/xod-client-electron/src/upload/actions.js +++ b/packages/xod-client-electron/src/upload/actions.js @@ -5,45 +5,51 @@ import uploadToEspruino from 'xod-espruino-upload'; import { UPLOAD } from './actionTypes'; -export const upload = () => (dispatch, getState) => { - const state = getState(); +const progressUpload = (dispatch, id) => (message, percentage) => dispatch( + client.progressProcess( + id, + UPLOAD, + { message, percentage } + ) +); +const succeedUpload = (dispatch, id) => (message = '') => dispatch( + client.successProcess(id, UPLOAD, { message }) +); +const failUpload = (dispatch, id) => err => dispatch( + client.failProcess(id, UPLOAD, { message: err.message }) +); + +const getTranspiledCode = (transpiler, state) => { const project = client.getProject(state); - const curPatchPath = client.getCurrentPatchPath(state); - const eitherCode = transpileForEspruino(project, curPatchPath); + const curPatchId = client.getCurrentPatchId(state); + return transpiler(project, curPatchId); +}; - const newId = dispatch(client.addProcess(UPLOAD)); - - const progress = (message, percentage) => dispatch(client.progressProcess( - newId, - UPLOAD, - { - message, - percentage, - } - )); - - const succeed = () => dispatch(client.successProcess( - newId, - UPLOAD - )); - - const fail = err => dispatch(client.failProcess( - newId, - UPLOAD, - { message: err.message } - )); +export const upload = () => (dispatch, getState) => { + const code = getTranspiledCode(transpileForEspruino, getState()); + const processId = dispatch(client.addProcess(UPLOAD)); foldEither( fail, (code) => { - uploadToEspruino(code, progress) - .then(succeed) - .catch(fail); + uploadToEspruino(code, progressUpload(dispatch, processId)) + .then(succeedUpload(dispatch, processId)) + .catch(failUpload(dispatch, processId)); }, eitherCode ); }; +export const uploadToArduino = () => (dispatch) => { + const processId = dispatch(client.addProcess(UPLOAD)); + return { + success: succeedUpload(dispatch, processId), + fail: failUpload(dispatch, processId), + progress: progressUpload(dispatch, processId), + }; +}; + export default { upload, + uploadToArduino, }; diff --git a/packages/xod-client-electron/src/view/containers/App.jsx b/packages/xod-client-electron/src/view/containers/App.jsx index c72014ed..62c9cd35 100644 --- a/packages/xod-client-electron/src/view/containers/App.jsx +++ b/packages/xod-client-electron/src/view/containers/App.jsx @@ -7,6 +7,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { HotKeys } from 'react-hotkeys'; import EventListener from 'react-event-listener'; +import { ipcRenderer, remote as remoteElectron } from 'electron'; import client from 'xod-client'; import { @@ -18,7 +19,7 @@ import * as actions from '../actions'; import * as uploadActions from '../../upload/actions'; import { getUploadProcess } from '../../upload/selectors'; import { SAVE_PROJECT } from '../actionTypes'; -import { UPLOAD } from '../../upload/actionTypes'; +import { UPLOAD, UPLOAD_TO_ARDUINO } from '../../upload/actionTypes'; import PopupSetWorkspace from '../../settings/components/PopupSetWorkspace'; import PopupProjectSelection from '../../projects/components/PopupProjectSelection'; import PopupUploadProject from '../../upload/components/PopupUploadProject'; @@ -27,9 +28,7 @@ import { getSettings, getWorkspace } from '../../settings/selectors'; import { changeWorkspace, checkWorkspace } from '../../settings/actions'; import { SaveProgressBar } from '../components/SaveProgressBar'; -// TODO: tweak webpack config to allow importing built-in electron package -const { app, dialog, Menu } = window.require('electron').remote; - +const { app, dialog, Menu } = remoteElectron; const DEFAULT_CANVAS_WIDTH = 800; const DEFAULT_CANVAS_HEIGHT = 600; @@ -52,6 +51,7 @@ class App extends client.App { this.onResize = this.onResize.bind(this); this.onUpload = this.onUpload.bind(this); + this.onUploadToArduino = this.onUploadToArduino.bind(this); this.onShowCodeEspruino = this.onShowCodeEspruino.bind(this); this.onShowCodeNodejs = this.onShowCodeNodejs.bind(this); this.onShowCodeArduino = this.onShowCodeArduino.bind(this); @@ -105,6 +105,39 @@ class App extends client.App { this.props.actions.upload(); } + onUploadToArduino(pab) { + const { project, currentPatchId } = this.props; + const proc = this.props.actions.uploadToArduino(); + + this.showUploadProgressPopup(); + ipcRenderer.send(UPLOAD_TO_ARDUINO, { + pab, + project, + patchId: currentPatchId, + }); + ipcRenderer.on(UPLOAD_TO_ARDUINO, (event, payload) => { + // Success + if (payload.code === 0 && payload.type === 'UPLOAD') { + proc.success(payload.message); + return; + } + // Progress + if (payload.code === 0 && payload.type !== 'UPLOAD') { + proc.progress(payload.message, payload.percentage); + return; + } + // Error + if (payload.code === 1) { + proc.fail(payload); + if (payload.type === 'IDE') { + // TODO: show popup + console.error('Arduino IDE not found!'); + } + return; + } + }); + } + onCreateProject(projectName) { this.props.actions.createProject(projectName); this.hidePopupCreateProject(); @@ -249,6 +282,28 @@ class App extends client.App { onClick(items.showCodeForNodeJS, this.onShowCodeNodejs), items.separator, onClick(items.showCodeForArduino, this.onShowCodeArduino), + // TODO: Remove this hardcode and do a magic in the xod-arduino-builder + onClick(items.uploadToArduinoUno, () => this.onUploadToArduino( + { + package: 'arduino', + architecture: 'avr', + board: 'uno', + } + )), + onClick(items.uploadToArduinoLeonardo, () => this.onUploadToArduino( + { + package: 'arduino', + architecture: 'avr', + board: 'leonardo', + } + )), + onClick(items.uploadToArduinoM0, () => this.onUploadToArduino( + { + package: 'arduino', + architecture: 'samd', + board: 'mzero_bl', + } + )), ] ), ]; @@ -443,6 +498,7 @@ const mapDispatchToProps = dispatch => ({ loadProject: actions.loadProject, importProject: client.importProject, // used in base App class upload: uploadActions.upload, + uploadToArduino: uploadActions.uploadToArduino, addError: client.addError, deleteProcess: client.deleteProcess, createPatch: client.requestCreatePatch, diff --git a/packages/xod-client/src/project/initialProjectStateWithStdlib.js b/packages/xod-client/src/project/initialProjectStateWithStdlib.js index c65226d9..8546eebb 100644 --- a/packages/xod-client/src/project/initialProjectStateWithStdlib.js +++ b/packages/xod-client/src/project/initialProjectStateWithStdlib.js @@ -1569,19 +1569,19 @@ export default { nodes: {}, path: 'xod/core/digital_input', pins: { - pin: { + PIN: { description: '', direction: 'input', - key: 'pin', + key: 'PIN', label: 'pin', order: 0, type: 'string', value: '', }, - value: { + VALUE: { description: '', direction: 'output', - key: 'value', + key: 'VALUE', label: 'value', order: 0, type: 'number', @@ -1598,19 +1598,19 @@ export default { nodes: {}, path: 'xod/core/digital_output', pins: { - pin: { + PIN: { description: '', direction: 'input', - key: 'pin', + key: 'PIN', label: 'pin', order: 0, type: 'number', value: 0, }, - value: { + VALUE: { description: '', direction: 'input', - key: 'value', + key: 'VALUE', label: 'value', order: 1, type: 'boolean', diff --git a/packages/xod-client/src/utils/menu.js b/packages/xod-client/src/utils/menu.js index ec8a2e7b..e15dcf3a 100644 --- a/packages/xod-client/src/utils/menu.js +++ b/packages/xod-client/src/utils/menu.js @@ -84,6 +84,18 @@ const rawItems = { key: 'showCodeForArduino', label: 'Show Code For Arduino', }, + uploadToArduinoUno: { + key: 'uploadToArduinoUno', + label: 'Upload to Arduino Uno', + }, + uploadToArduinoLeonardo: { + key: 'uploadToArduinoLeonardo', + label: 'Upload to Arduino Leonardo', + }, + uploadToArduinoM0: { + key: 'uploadToArduinoM0', + label: 'Upload to Arduino M0', + }, }; const assignHotkeys = menuItem => R.when(