robopro-link
Version:
Porvide local hardware function to RoboPro
263 lines (224 loc) • 10.3 kB
JavaScript
const fs = require('fs');
const {spawn, spawnSync} = require('child_process');
const path = require('path');
const ansi = require('ansi-string');
const yaml = require('js-yaml');
const os = require('os');
const ARDUINO_CLI_STDOUT_GREEN_START = /Reading \||Writing \|/g;
const ARDUINO_CLI_STDOUT_GREEN_END = /%/g;
const ARDUINO_CLI_STDOUT_WHITE = /avrdude done/g;
const ARDUINO_CLI_STDOUT_RED_START = /can't open device|programmer is not responding/g;
const ARDUINO_CLI_STDERR_RED_IGNORE = /Executable segment sizes/g;
const ABORT_STATE_CHECK_INTERVAL = 100;
class Arduino {
constructor (peripheralPath, config, userDataPath, toolsPath, sendstd) {
this._peripheralPath = peripheralPath;
this._config = config;
this._userDataPath = userDataPath;
this._arduinoPath = path.join(toolsPath, 'Arduino');
this._sendstd = sendstd;
this._firmwareDir = path.join(toolsPath, '../firmwares/arduino');
this._abort = false;
// If the fqbn is an object means the value of this parameter is
// different under different systems.
if (typeof this._config.fqbn === 'object') {
this._config.fqbn = this._config.fqbn[os.platform()];
}
const projectPathName = `${this._config.fqbn.replace(/:/g, '_')}_project`.split(/_/).splice(0, 3)
.join('_');
this._configFilePath = path.join(this._userDataPath, 'arduino/arduino-cli.yaml');
this._projectFilePath = path.join(this._userDataPath, 'arduino', projectPathName);
this._arduinoCliPath = path.join(this._arduinoPath, 'arduino-cli');
this._codeFolderPath = path.join(this._projectFilePath, 'code');
this._codeFilePath = path.join(this._codeFolderPath, 'code.ino');
this._buildPath = path.join(this._projectFilePath, 'build');
this._buildCachePath = path.join(this._projectFilePath, 'buildCache');
this.initArduinoCli();
}
initArduinoCli () {
// try to init the arduino cli config.
spawnSync(this._arduinoCliPath, ['config', 'init', '--dest-file', this._configFilePath]);
// if arduino cli config haven be init, set it to link arduino path.
const buf = spawnSync(this._arduinoCliPath, ['config', 'dump', '--config-file', this._configFilePath]);
try {
if (buf.error) {
throw buf.error;
}
const stdout = yaml.load(buf.stdout.toString());
if (stdout.directories.data !== this._arduinoPath) {
this._sendstd(`${ansi.yellow_dark}arduino cli config has not been initialized yet.\n`);
this._sendstd(`${ansi.green_dark}set the path to ${this._arduinoPath}.\n`);
spawnSync(this._arduinoCliPath, ['config', 'set', 'directories.data', this._arduinoPath,
'--config-file', this._configFilePath]);
spawnSync(this._arduinoCliPath, ['config', 'set', 'directories.downloads',
path.join(this._arduinoPath, 'staging'), '--config-file', this._configFilePath]);
spawnSync(this._arduinoCliPath, ['config', 'set', 'directories.user', this._arduinoPath,
'--config-file', this._configFilePath]);
}
} catch (err) {
this._sendstd(`${ansi.red}arduino cli init error:${err.toString()}\n`);
}
}
abortUpload () {
this._abort = true;
}
build (code) {
return new Promise((resolve, reject) => {
if (!fs.existsSync(this._codeFolderPath)) {
fs.mkdirSync(this._codeFolderPath, {recursive: true});
}
try {
fs.writeFileSync(this._codeFilePath, code);
} catch (err) {
return reject(err);
}
const args = [
'compile',
'--fqbn', this._config.fqbn,
'--libraries', path.join(this._arduinoPath, 'libraries'),
'--warnings=none',
'--verbose',
'--build-path', this._buildPath,
'--build-cache-path', this._buildCachePath,
'--config-file', this._configFilePath,
this._codeFolderPath
];
// if extensions library to not empty
this._config.library.forEach(lib => {
if (fs.existsSync(lib)) {
args.splice(3, 0, '--libraries', lib);
}
});
const arduinoCli = spawn(this._arduinoCliPath, args);
this._sendstd(`Start building...\n`);
arduinoCli.stderr.on('data', buf => {
const data = buf.toString();
if (data.search(ARDUINO_CLI_STDERR_RED_IGNORE) !== -1) { // eslint-disable-line no-negated-condition
this._sendstd(ansi.red + data);
} else {
this._sendstd(ansi.red + data);
}
});
arduinoCli.stdout.on('data', buf => {
const data = buf.toString();
let ansiColor = null;
if (data.search(/Sketch uses|Global variables/g) === -1) {
ansiColor = ansi.clear;
} else {
ansiColor = ansi.green_dark;
}
this._sendstd(ansiColor + data);
});
const listenAbortSignal = setInterval(() => {
if (this._abort) {
arduinoCli.kill();
}
}, ABORT_STATE_CHECK_INTERVAL);
arduinoCli.on('exit', outCode => {
clearInterval(listenAbortSignal);
this._sendstd(`${ansi.clear}\r\n`); // End ansi color setting
switch (outCode) {
case null:
// process be killed, do nothing.
return resolve('Aborted');
case 0:
return resolve('Success');
case 1:
return reject(new Error('Build failed'));
case 2:
return reject(new Error('Sketch not found'));
case 3:
return reject(new Error('Invalid (argument for) commandline optiond'));
case 4:
return reject(new Error('Preference passed to --get-pref does not exist'));
default:
return reject(new Error('Unknown error'));
}
});
});
}
_insertStr (soure, start, newStr) {
return soure.slice(0, start) + newStr + soure.slice(start);
}
async flash (firmwarePath = null) {
const args = [
'upload',
'--fqbn', this._config.fqbn,
'--verbose',
'--verify',
'--config-file', this._configFilePath,
`-p${this._peripheralPath}`
];
// for k210 we must specify the programmer used as kflash
if (this._config.fqbn.startsWith('Maixduino:k210:')) {
args.push('-Pkflash');
}
if (firmwarePath) {
args.push('--input-file', firmwarePath, firmwarePath);
} else {
args.push('--input-dir', this._buildPath);
args.push(this._codeFolderPath);
}
return new Promise((resolve, reject) => {
const arduinoCli = spawn(this._arduinoCliPath, args);
arduinoCli.stderr.on('data', buf => {
let data = buf.toString();
// todo: Because the feacture of avrdude sends STD information intermittently.
// There should be a better way to handle these mesaage.
if (data.search(ARDUINO_CLI_STDOUT_GREEN_START) !== -1) {
data = this._insertStr(data, data.search(ARDUINO_CLI_STDOUT_GREEN_START), ansi.green_dark);
}
if (data.search(ARDUINO_CLI_STDOUT_GREEN_END) !== -1) {
data = this._insertStr(data, data.search(ARDUINO_CLI_STDOUT_GREEN_END) + 1, ansi.clear);
}
if (data.search(ARDUINO_CLI_STDOUT_WHITE) !== -1) {
data = this._insertStr(data, data.search(ARDUINO_CLI_STDOUT_WHITE), ansi.clear);
}
if (data.search(ARDUINO_CLI_STDOUT_RED_START) !== -1) {
data = this._insertStr(data, data.search(ARDUINO_CLI_STDOUT_RED_START), ansi.red);
}
this._sendstd(data);
});
arduinoCli.stdout.on('data', buf => {
// It seems that avrdude didn't use stdout.
const data = buf.toString();
this._sendstd(data);
});
const listenAbortSignal = setInterval(() => {
if (this._abort) {
if (os.platform() === 'win32') {
spawnSync('taskkill', ['/pid', arduinoCli.pid, '/f', '/t']);
} else {
arduinoCli.kill();
}
}
}, ABORT_STATE_CHECK_INTERVAL);
arduinoCli.on('exit', code => {
clearInterval(listenAbortSignal);
const wait = ms => new Promise(relv => setTimeout(relv, ms));
switch (code) {
case 0:
if (this._config.postUploadDelay) {
// Waiting for usb rerecognize.
wait(this._config.postUploadDelay).then(() => resolve('Success'));
} else {
return resolve('Success');
}
break;
case 1:
if (this._abort) {
// Wait for 100ms before returning to prevent the serial port from being released.
wait(100).then(() => resolve('Aborted'));
} else {
return reject(new Error('avrdude failed to flash'));
}
}
});
});
}
flashRealtimeFirmware () {
const firmwarePath = path.join(this._firmwareDir, this._config.firmware);
return this.flash(firmwarePath);
}
}
module.exports = Arduino;