feat(xod-client, xod-client-electron): add functionality of detecting installed Arduino IDE (left todo to add a popup), installing toolchain, transpiling, detecting arduino device (hardcoded vendorId) and uploading code to device with showing stderr/stdout of arduino. Also added few menu items to run this functionality.

This commit is contained in:
Kirill Shumilov
2017-04-18 16:38:52 +03:00
parent 0634c10eeb
commit 1982e46f71
8 changed files with 287 additions and 40 deletions

View File

@@ -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",

View File

@@ -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();
};

View File

@@ -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,
};

View File

@@ -1,5 +1,7 @@
export const UPLOAD = 'UPLOAD';
export const UPLOAD_TO_ARDUINO = 'UPLOAD_TO_ARDUINO';
export default {
UPLOAD,
UPLOAD_TO_ARDUINO,
};

View File

@@ -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,
};

View File

@@ -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,

View File

@@ -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',

View File

@@ -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(