mirror of
https://github.com/xodio/xod.git
synced 2026-03-14 04:36:51 +01:00
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:
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
123
packages/xod-client-electron/src/app/uploadActions.js
Normal file
123
packages/xod-client-electron/src/app/uploadActions.js
Normal 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,
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
export const UPLOAD = 'UPLOAD';
|
||||
export const UPLOAD_TO_ARDUINO = 'UPLOAD_TO_ARDUINO';
|
||||
|
||||
export default {
|
||||
UPLOAD,
|
||||
UPLOAD_TO_ARDUINO,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user