diff --git a/.circleci/config.yml b/.circleci/config.yml index 683ec72ca..cd4f7743e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -37,9 +37,9 @@ defs: step-install-arduino-cli-on-mac: &step-install-arduino-cli-on-mac name: Install arduino-cli command: | - curl -s --create-dirs -o "$HOME/arduino-cli.zip" "https://downloads.arduino.cc/arduino-cli/arduino-cli-0.2.2-alpha.preview-osx.zip" \ + curl -s --create-dirs -o "$HOME/arduino-cli.zip" "https://downloads.arduino.cc/arduino-cli/arduino-cli-0.3.1-alpha.preview-osx.zip" \ && unzip "$HOME/arduino-cli.zip" -d "$HOME" \ - && mv "$HOME/arduino-cli-0.2.2-alpha.preview-osx" "$HOME/arduino-cli" \ + && mv "$HOME/arduino-cli-0.3.1-alpha.preview-osx" "$HOME/arduino-cli" \ && cp "$HOME/arduino-cli" "./packages/xod-client-electron/arduino-cli" \ && mkdir "/tmp/arduino-cli" \ && cp "$HOME/arduino-cli" "/tmp/arduino-cli/arduino-cli" @@ -47,9 +47,9 @@ defs: step-install-arduino-cli-on-linux: &step-install-arduino-cli-on-linux name: Install arduino-cli command: | - curl -s --create-dirs -o "$HOME/arduino-cli.tar.bz2" "https://downloads.arduino.cc/arduino-cli/arduino-cli-0.2.2-alpha.preview-linux64.tar.bz2" \ + curl -s --create-dirs -o "$HOME/arduino-cli.tar.bz2" "https://downloads.arduino.cc/arduino-cli/arduino-cli-0.3.1-alpha.preview-linux64.tar.bz2" \ && tar xvjf "$HOME/arduino-cli.tar.bz2" -C "$HOME" \ - && mv "$HOME/arduino-cli-0.2.2-alpha.preview-linux64" "$HOME/arduino-cli" \ + && mv "$HOME/arduino-cli-0.3.1-alpha.preview-linux64" "$HOME/arduino-cli" \ && cp "$HOME/arduino-cli" "./packages/xod-client-electron/arduino-cli" \ && mkdir "/tmp/arduino-cli" \ && cp "$HOME/arduino-cli" "/tmp/arduino-cli/arduino-cli" diff --git a/packages/arduino-cli/README.md b/packages/arduino-cli/README.md index 17dd4b3ef..5a66a7925 100644 --- a/packages/arduino-cli/README.md +++ b/packages/arduino-cli/README.md @@ -2,7 +2,7 @@ A javascript wrapper over the [arduino-cli](https://github.com/arduino/arduino-cli) tool -**Works on Arduino-cli 0.2.2-alpha.preview** +**Works on Arduino-cli 0.3.1-alpha.preview** ## How to use @@ -177,10 +177,20 @@ Accepts: - `usbID` `String` — An ID of the board (VID, PID). For example, `2341:0042` ### InstalledBoard -- `name` `` — A board name with an added cpu option name, if it exists. - For example, "Arduino/Genuino Uno" or "Arduino/Genuino Mega2560 (ATmega2560 (Mega 2560))" -- `fqbn` `` — A fully-qualified board name. - For example, `arduino:avr:uno` or `arduino:avr:mega2560:cpu=atmega2560` +- `name` `` — A board name + E.G. "Arduino/Genuino Uno" or "Arduino/Genuino Mega2560" +- `fqbn` `` — A fully-qualified board name + E.G. "arduino:avr:uno" or "arduino:avr:mega2560" +- `options` `>` — a list of options for this board + +### Option +Object, that represents one group of the board option. For example, CPU Frequency. +- `optionName` `` — A human-readable name of the option group ("CPU Frequency") +- `optionId` `` — An id of the option from `boards.txt`. E.G. `CpuFrequency` +- `values` `>` — a list of option values, that represented as objects + with two fields + - `name` `` — A human-readable option name ("80 MHz") + - `value` `` — A value, that will be used by tools. ("80") ### AvailableBoard - `name` `` — A board name (e.g., "Arduino/Genuino Mega2560") diff --git a/packages/arduino-cli/src/cpuParser.js b/packages/arduino-cli/src/cpuParser.js deleted file mode 100644 index 5c83bc41a..000000000 --- a/packages/arduino-cli/src/cpuParser.js +++ /dev/null @@ -1,153 +0,0 @@ -/** - * One bright day this module will be burned. - * See: https://github.com/arduino/arduino-cli/issues/45 - */ - -import path from 'path'; -import * as R from 'ramda'; -import * as fse from 'fs-extra'; -import promiseAllProperties from 'promise-all-properties'; - -const PACKAGES_DIR = 'packages'; -const HARDWARE_DIR = 'hardware'; -const BOARDS_FNAME = 'boards.txt'; - -// :: Path -> FQBN -> String -> Path -export const getBoardsTxtPath = R.curry((dataPath, fqbn, version) => { - const [packageName, archName] = R.split(':', fqbn); - return path.resolve( - dataPath, - PACKAGES_DIR, - packageName, - HARDWARE_DIR, - archName, - version, - BOARDS_FNAME - ); -}); - -/** - * Parses Arduino's `.txt` definition file. Like `boards.txt` or `platform.txt`. - * - * In the reduce function we have to store value of `A.menu.cpu.B` into special - * property to avoid breaking it with associating other properties. - * E.G. - * `nano.menu.cpu.atmega328 = ATmega328` - * stores String 'ATmega328' by the path `menu.cpu.atmega328` - * but then config has next lines: - * `nano.menu.cpu.atmega328.upload.speed = 12800` - * so our string will be converted into object in which will be associated - * object `upload` with property `speed`. - * As a result we have something like this: - * ``` - * { - * menu: { - * cpu: { - * atmega328: { - * 0: 'A', - * 1: 'T', - * 2: 'm', - * ... - * upload: { - * speed: '12800' - * } - * } - * } - * } - * } - * ``` - * To prevent it we'll store `nano.menu.cpu.atmega328 = ATmega328` into - * special property `cpuName` and then we have: - * ``` - * { - * atmega328: { - * cpuName: 'ATmega328', - * upload: { - * speed: '12800' - * } - * } - * } - * ``` - */ -// :: String -> Object -const parseTxtConfig = R.compose( - R.reduce( - (acc, [tokens, value]) => - R.compose( - R.assocPath(R.__, value, acc), - R.when( - R.both(R.propEq(1, 'menu'), R.compose(R.equals(4), R.length)), - R.append('cpuName') - ) - )(tokens), - {} - ), - R.map(R.compose(R.zipWith(R.call, [R.split('.'), R.identity]), R.split('='))), - R.reject(R.test(/^(#|$)/)), - R.map(R.trim), - R.split(/$/gm) -); - -// :: Path -> Promise Object Error -const readBoardsTxt = boardsTxtPath => - R.pipeP(() => fse.readFile(boardsTxtPath, 'utf8'), parseTxtConfig)(); - -/** - * Returns a patched list of boards, where could be added/replaced some - * boards with FQBN and Names included cpu options. - * E.G. - * `Arduino/Geuino Uno` will be left untouched. - * But one item `Arduino/Genuino Mega or Mega 2560` will be replaced - * with two new items: - * `Arduino/Genuino Mega or Mega 2560 (ATmega2560 (Mega 2560))` with FQBN `arduino:avr:mega:cpu=atmega2560` - * `Arduino/Genuino Mega or Mega 2560 (ATmega1280)` with FQBN `arduino:avr:mega:cpu=atmega1280` - * - * Other fields of boards will be untouched and cloned between the same items. - * - * Core :: { ID:: String, Installed:: String } - * :: Path -> [Core] -> Nullable [{fqbn, name}] -> Promise [{fqbn, name}] Error - */ -export const patchBoardsWithCpu = R.curry(async (dataPath, cores, boards) => { - if (!boards) return []; - - // Map CoreID Object - const coreBoardPrefs = await R.compose( - promiseAllProperties, - R.map(txtPath => readBoardsTxt(txtPath)), - R.map(core => getBoardsTxtPath(dataPath, core.ID, core.Installed)), - R.indexBy(R.prop('ID')) - )(cores); - - // Map CoreId [{FQBN, Board Name}] - const boardsByCoreId = R.compose( - R.groupBy(R.pipe(R.prop('fqbn'), R.split(':'), R.take(2), R.join(':'))), - R.reject(R.propSatisfies(R.isEmpty, 'fqbn')) // reject "unknown" boards - )(boards); - - const patchedBoards = R.mapObjIndexed((boardList, coreId) => - R.compose( - R.unnest, - R.map(board => { - const boardKey = R.pipe(R.prop('fqbn'), R.split(':'), R.last)(board); - const cpuOptions = R.path( - [coreId, boardKey, 'menu', 'cpu'], - coreBoardPrefs - ); - return !cpuOptions - ? [board] - : R.reduce((acc, cpuOpt) => { - const newBoard = R.evolve( - { - name: name => `${name} (${cpuOpt.cpuName})`, - fqbn: fqbn => `${fqbn}:cpu=${cpuOpt.build.mcu}`, - }, - board - ); - return R.append(newBoard, acc); - }, [])(R.values(cpuOptions)); - }) - )(boardList) - )(boardsByCoreId); - - return R.pipe(R.values, R.unnest)(patchedBoards); -}); diff --git a/packages/arduino-cli/src/index.js b/packages/arduino-cli/src/index.js index b32fce5e5..566cc00e6 100644 --- a/packages/arduino-cli/src/index.js +++ b/packages/arduino-cli/src/index.js @@ -3,10 +3,10 @@ import * as R from 'ramda'; import { resolve } from 'path'; import { exec, spawn } from 'child-process-promise'; import YAML from 'yamljs'; +import { remove } from 'fs-extra'; import { configure, addPackageIndexUrl, addPackageIndexUrls } from './config'; -import parseTable from './parseTable'; -import { patchBoardsWithCpu } from './cpuParser'; +import { patchBoardsWithOptions } from './optionParser'; import listAvailableBoards from './listAvailableBoards'; import parseProgressLog from './parseProgressLog'; @@ -47,19 +47,27 @@ const ArduinoCli = (pathToBin, config = null) => { const sketch = name => resolve(cfg.sketchbook_path, name); - const runAndParseTable = args => run(args).then(parseTable); const runAndParseJson = args => run(args).then(JSON.parse); + const listCores = () => + run('core list --format json') + .then(R.when(R.isEmpty, R.always('{}'))) + .then(JSON.parse) + .then(R.propOr([], 'Platforms')) + .then(R.map(R.over(R.lensProp('ID'), R.replace(/(@.+)$/, '')))); + const listBoardsWith = (listCmd, boardsGetter) => Promise.all([ - runAndParseTable('core list'), + listCores(), runAndParseJson(`board ${listCmd} --format json`), ]).then(([cores, boards]) => - patchBoardsWithCpu(cfg.arduino_data, cores, boardsGetter(boards)) + patchBoardsWithOptions(cfg.arduino_data, cores, boardsGetter(boards)) ); + const getConfig = () => run('config dump').then(YAML.parse); + return { - dumpConfig: () => run('config dump').then(YAML.parse), + dumpConfig: getConfig, listConnectedBoards: () => listBoardsWith('list', R.prop('serialBoards')), listInstalledBoards: () => listBoardsWith('listall', R.prop('boards')), listAvailableBoards: () => listAvailableBoards(cfg.arduino_data), @@ -79,20 +87,30 @@ const ArduinoCli = (pathToBin, config = null) => { ), core: { download: (onProgress, pkgName) => - runWithProgress( - parseProgressLog(onProgress), - `core download ${pkgName}` + // TODO: + // Get rid of `remove` the staging directory when + // arduino-cli fix issue https://github.com/arduino/arduino-cli/issues/43 + remove(resolve(cfg.arduino_data, 'staging')).then(() => + runWithProgress( + parseProgressLog(onProgress), + `core download ${pkgName}` + ) ), install: (onProgress, pkgName) => - runWithProgress( - parseProgressLog(onProgress), - `core install ${pkgName}` + // TODO: + // Get rid of `remove` the staging directory when + // arduino-cli fix issue https://github.com/arduino/arduino-cli/issues/43 + remove(resolve(cfg.arduino_data, 'staging')).then(() => + runWithProgress( + parseProgressLog(onProgress), + `core install ${pkgName}` + ) ), - // We have to call our custon `parseTable` - // until bug with `--format json` in arduino-cli will be fixed - // https://github.com/arduino/arduino-cli/issues/39 - list: () => runAndParseTable('core list'), - search: query => runAndParseTable(`core search ${query}`), + list: listCores, + search: query => + run(`core search ${query} --format json`) + .then(R.prop('Platforms')) + .then(R.defaultTo([])), uninstall: pkgName => run(`core uninstall ${pkgName}`), updateIndex: () => run('core update-index'), upgrade: () => run('core upgrade'), diff --git a/packages/arduino-cli/src/optionParser.js b/packages/arduino-cli/src/optionParser.js new file mode 100644 index 000000000..32c529cb6 --- /dev/null +++ b/packages/arduino-cli/src/optionParser.js @@ -0,0 +1,185 @@ +/** + * One bright day this module will be burned. + * See: https://github.com/arduino/arduino-cli/issues/45 + */ + +import path from 'path'; +import * as R from 'ramda'; +import * as fse from 'fs-extra'; +import promiseAllProperties from 'promise-all-properties'; + +const PACKAGES_DIR = 'packages'; +const HARDWARE_DIR = 'hardware'; +const BOARDS_FNAME = 'boards.txt'; + +/** + * Types + * + * OptionValue :: { + * name: String, // Human-readable name. E.G. "80 MHz" + * value: String, // Option value. E.G. "80" + * } + * + * OptionName :: String // Human-readable option name. E.G. "CPU Frequency" + * OptionId :: String // Option id as is in the `boards.txt`, E.G. "CpuFrequency" + * + * Option :: { + * optionName: OptionName, + * optionId: OptionId, + * values: [OptionValue], + * } + */ + +// ============================================================================= +// +// Utils +// +// ============================================================================= + +// :: String -> [String] +export const getLines = R.compose( + R.reject(R.test(/^(#|$)/)), + R.map(R.trim), + R.split(/$/gm) +); + +const menuRegExp = /^menu\./; + +const optionNameRegExp = /^menu\.([a-zA-Z0-9_]+)=([a-zA-Z0-9-_ ]+)$/; + +const boardOptionRegExp = /^([a-zA-Z0-9_]+)\.menu\.([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)=([a-zA-Z0-9-_ ()]+)$/; + +const osRegExp = /(linux|macosx|windows)/; + +// ============================================================================= +// +// Parsers +// +// ============================================================================= + +// :: Path -> FQBN -> String -> Path +export const getBoardsTxtPath = R.curry((dataPath, fqbn, version) => { + const [packageName, archName] = R.split(':', fqbn); + return path.resolve( + dataPath, + PACKAGES_DIR, + packageName, + HARDWARE_DIR, + archName, + version, + BOARDS_FNAME + ); +}); + +/** + * Parses human-readable option names from `boards.txt` contents. + * + * E.G. + * `menu.CpuFrequency=CPU Frequency` + * will become + * `{ CpuFrequency: 'CPU Frequency' }` + * + * :: [String] -> Map OptionId OptionName + */ +export const parseOptionNames = R.compose( + R.fromPairs, + R.map(R.pipe(R.match(optionNameRegExp), R.tail)), + R.filter(R.test(menuRegExp)) +); + +/** + * Parses options for boards indexed by board ID ("uno", "wifi_slot" and etc) + * + * :: [String] -> Map BoardId (Map OptionId [OptionValue]) + */ +export const parseIntermediateOptions = R.compose( + R.reduce((acc, line) => { + const boardOption = R.match(boardOptionRegExp, line); + if (boardOption.length < 5) return acc; + const [, boardId, optionId, optionVal, optionName] = boardOption; + const option = { name: optionName, value: optionVal }; + return R.over(R.lensPath([boardId, optionId]), R.append(option), acc); + }, {}), + R.reject(R.either(R.test(menuRegExp), R.test(osRegExp))) +); + +// :: Map OptionId OptionName -> Map OptionId [OptionValue] -> [Option] +export const convertIntermediateOptions = R.curry((optionNames, intOptions) => + R.compose( + R.values, + R.mapObjIndexed((val, key) => ({ + optionName: optionNames[key], + optionId: key, + values: val, + })) + )(intOptions) +); + +// :: String -> Map BoardId [Option] +/** + * Parses boards.txt options into Object, that could be merged with Board objects + * by board id (last part of FQBN). + * + * :: String -> Map BoardId [Option] + */ +export const parseOptions = R.compose(lines => { + const optionNames = parseOptionNames(lines); + const options = parseIntermediateOptions(lines); + return R.map(convertIntermediateOptions(optionNames), options); +}, getLines); + +/** + * Converts { name, fqbn } objects into InstalledBoard objects. + * + * If there is no options for the board or it is not installed yet, it will + * be returned untouched. + * + * :: Map BoardId [Option] -> (InstalledBoard | AvailableBoard) -> (InstalledBoard | AvailableBoard) + */ +export const patchBoardWithOptions = R.curry((boardsTxtContent, board) => { + const options = parseOptions(boardsTxtContent); + + if (!R.has('fqbn', board)) return board; + const boardId = R.pipe(R.prop('fqbn'), R.split(':'), R.last)(board); + return R.pipe(R.propOr([], boardId), R.assoc('options', R.__, board))( + options + ); +}); + +// ============================================================================= +// +// API +// +// ============================================================================= + +/** + * Loads `boards.txt` of installed cores and patches Board objects with options. + * + * :: Path -> [Core] -> [InstalledBoard | AvailableBoard] -> [InstalledBoard | AvailableBoard] + */ +export const patchBoardsWithOptions = R.curry( + async (dataPath, cores, boards) => { + if (!boards) return []; + + // Map CoreID Object + const boardTxtContentsByCoreId = await R.compose( + promiseAllProperties, + R.map(txtPath => fse.readFile(txtPath, 'utf8')), + R.map(core => getBoardsTxtPath(dataPath, core.ID, core.Installed)), + R.indexBy(R.prop('ID')) + )(cores); + + return R.map(board => { + if (!R.has('fqbn', board)) return board; + + const coreId = R.compose( + R.join(':'), + R.init, + R.split(':'), + R.prop('fqbn') + )(board); + const boardTxtContents = R.propOr('', coreId, boardTxtContentsByCoreId); + return patchBoardWithOptions(boardTxtContents, board); + }, boards); + } +); diff --git a/packages/arduino-cli/src/parseTable.js b/packages/arduino-cli/src/parseTable.js deleted file mode 100644 index b63773147..000000000 --- a/packages/arduino-cli/src/parseTable.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * One bright day this module will be burned. - * See: https://github.com/arduino/arduino-cli/issues/39 - */ - -import * as R from 'ramda'; - -export default data => { - const table = R.compose( - R.map(R.compose(R.map(R.trim), R.split('\t'))), - R.split('\n'), - R.trim - )(data); - - const headers = table[0]; // first row are table headers - - return R.compose( - R.map( - R.addIndex(R.reduce)( - (acc, v, idx) => (!headers[idx] ? acc : R.assoc(headers[idx], v, acc)), - {} - ) - ), - R.tail - )(table); -}; diff --git a/packages/arduino-cli/test-func/index.spec.js b/packages/arduino-cli/test-func/index.spec.js index a2bd2c30b..8edc06df2 100644 --- a/packages/arduino-cli/test-func/index.spec.js +++ b/packages/arduino-cli/test-func/index.spec.js @@ -114,6 +114,7 @@ describe('Arduino Cli', () => { assert.lengthOf(res, 1); assert.propertyVal(res[0], 'ID', 'arduino:avr'); assert.propertyVal(res[0], 'Installed', '1.6.21'); + assert.property(res[0], 'Latest'); assert.propertyVal(res[0], 'Name', 'Arduino AVR Boards'); })); it('Lists all installed boards with cpu options', () => @@ -122,14 +123,27 @@ describe('Arduino Cli', () => { { name: 'Arduino/Genuino Uno', fqbn: 'arduino:avr:uno', + options: [], }, { - name: 'Arduino/Genuino Mega or Mega 2560 (ATmega2560 (Mega 2560))', - fqbn: 'arduino:avr:mega:cpu=atmega2560', - }, - { - name: 'Arduino/Genuino Mega or Mega 2560 (ATmega1280)', - fqbn: 'arduino:avr:mega:cpu=atmega1280', + name: 'Arduino/Genuino Mega or Mega 2560', + fqbn: 'arduino:avr:mega', + options: [ + { + optionName: 'Processor', + optionId: 'cpu', + values: [ + { + name: 'ATmega2560 (Mega 2560)', + value: 'atmega2560', + }, + { + name: 'ATmega1280', + value: 'atmega1280', + }, + ], + }, + ], }, ]); })); diff --git a/packages/arduino-cli/test/cpuParser.spec.js b/packages/arduino-cli/test/cpuParser.spec.js deleted file mode 100644 index d23bc2bc8..000000000 --- a/packages/arduino-cli/test/cpuParser.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import path from 'path'; -import { assert } from 'chai'; - -import { getBoardsTxtPath, patchBoardsWithCpu } from '../src/cpuParser'; - -const tmpDir = path.resolve(__dirname, 'tmp'); -const fixturesDir = path.resolve(__dirname, 'fixtures'); - -const tmp = (...parts) => path.resolve(tmpDir, ...parts); - -describe('cpuParser', () => { - it('getBoardsTxtPath() returns correct path', () => { - assert.strictEqual( - getBoardsTxtPath(tmpDir, 'arduino:avr', '1.6.21'), - tmp('packages', 'arduino', 'hardware', 'avr', '1.6.21', 'boards.txt') - ); - }); - it('patchBoardsWithCpu() returns list of board objects with cpu options', () => { - const cores = [{ ID: 'arduino:avr', Installed: '1.6.21' }]; - const boards = [ - { fqbn: 'arduino:avr:uno', name: 'Arduino/Genuino Uno' }, - { - fqbn: 'arduino:avr:mega', - name: 'Arduino/Genuino Mega or Mega 2560', - }, - ]; - const expectedResult = [ - { - fqbn: 'arduino:avr:uno', - name: 'Arduino/Genuino Uno', - }, - { - fqbn: 'arduino:avr:mega:cpu=atmega2560', - name: 'Arduino/Genuino Mega or Mega 2560 (ATmega2560 (Mega 2560))', - }, - { - fqbn: 'arduino:avr:mega:cpu=atmega1280', - name: 'Arduino/Genuino Mega or Mega 2560 (ATmega1280)', - }, - ]; - - return patchBoardsWithCpu(fixturesDir, cores, boards).then(res => { - assert.deepEqual(res, expectedResult); - }); - }); -}); diff --git a/packages/arduino-cli/test/fixtures/packages/arduino/hardware/avr/1.6.21/boards.txt b/packages/arduino-cli/test/fixtures/packages/arduino/hardware/avr/1.6.21/boards.txt deleted file mode 100644 index fa208caad..000000000 --- a/packages/arduino-cli/test/fixtures/packages/arduino/hardware/avr/1.6.21/boards.txt +++ /dev/null @@ -1,93 +0,0 @@ -############################################################## - -uno.name=Arduino/Genuino Uno - -uno.vid.0=0x2341 -uno.pid.0=0x0043 -uno.vid.1=0x2341 -uno.pid.1=0x0001 -uno.vid.2=0x2A03 -uno.pid.2=0x0043 -uno.vid.3=0x2341 -uno.pid.3=0x0243 - -uno.upload.tool=avrdude -uno.upload.protocol=arduino -uno.upload.maximum_size=32256 -uno.upload.maximum_data_size=2048 -uno.upload.speed=115200 - -uno.bootloader.tool=avrdude -uno.bootloader.low_fuses=0xFF -uno.bootloader.high_fuses=0xDE -uno.bootloader.extended_fuses=0xFD -uno.bootloader.unlock_bits=0x3F -uno.bootloader.lock_bits=0x0F -uno.bootloader.file=optiboot/optiboot_atmega328.hex - -uno.build.mcu=atmega328p -uno.build.f_cpu=16000000L -uno.build.board=AVR_UNO -uno.build.core=arduino -uno.build.variant=standard - -############################################################## - -mega.name=Arduino/Genuino Mega or Mega 2560 - -mega.vid.0=0x2341 -mega.pid.0=0x0010 -mega.vid.1=0x2341 -mega.pid.1=0x0042 -mega.vid.2=0x2A03 -mega.pid.2=0x0010 -mega.vid.3=0x2A03 -mega.pid.3=0x0042 -mega.vid.4=0x2341 -mega.pid.4=0x0210 -mega.vid.5=0x2341 -mega.pid.5=0x0242 - -mega.upload.tool=avrdude -mega.upload.maximum_data_size=8192 - -mega.bootloader.tool=avrdude -mega.bootloader.low_fuses=0xFF -mega.bootloader.unlock_bits=0x3F -mega.bootloader.lock_bits=0x0F - -mega.build.f_cpu=16000000L -mega.build.core=arduino -mega.build.variant=mega -# default board may be overridden by the cpu menu -mega.build.board=AVR_MEGA2560 - -## Arduino/Genuino Mega w/ ATmega2560 -## ------------------------- -mega.menu.cpu.atmega2560=ATmega2560 (Mega 2560) - -mega.menu.cpu.atmega2560.upload.protocol=wiring -mega.menu.cpu.atmega2560.upload.maximum_size=253952 -mega.menu.cpu.atmega2560.upload.speed=115200 - -mega.menu.cpu.atmega2560.bootloader.high_fuses=0xD8 -mega.menu.cpu.atmega2560.bootloader.extended_fuses=0xFD -mega.menu.cpu.atmega2560.bootloader.file=stk500v2/stk500boot_v2_mega2560.hex - -mega.menu.cpu.atmega2560.build.mcu=atmega2560 -mega.menu.cpu.atmega2560.build.board=AVR_MEGA2560 - -## Arduino Mega w/ ATmega1280 -## ------------------------- -mega.menu.cpu.atmega1280=ATmega1280 - -mega.menu.cpu.atmega1280.upload.protocol=arduino -mega.menu.cpu.atmega1280.upload.maximum_size=126976 -mega.menu.cpu.atmega1280.upload.speed=57600 - -mega.menu.cpu.atmega1280.bootloader.high_fuses=0xDA -mega.menu.cpu.atmega1280.bootloader.extended_fuses=0xF5 -mega.menu.cpu.atmega1280.bootloader.file=atmega/ATmegaBOOT_168_atmega1280.hex - -mega.menu.cpu.atmega1280.build.mcu=atmega1280 -mega.menu.cpu.atmega1280.build.board=AVR_MEGA diff --git a/packages/arduino-cli/test/fixtures/packages/esp8266/hardware/esp8266/2.4.2/boards.txt b/packages/arduino-cli/test/fixtures/packages/esp8266/hardware/esp8266/2.4.2/boards.txt new file mode 100644 index 000000000..c0983dab7 --- /dev/null +++ b/packages/arduino-cli/test/fixtures/packages/esp8266/hardware/esp8266/2.4.2/boards.txt @@ -0,0 +1,88 @@ +#### JUST A PART OF BOARDS.TXT FILE FROM ESP8266 PACKAGE +#### SOME OPTIONS WAS REMOVED TO MINIFY FIXTURE +#### ONLY FOR TESTS! + +# +# Do not create pull-requests for this file only, CI will not accept them. +# You *must* edit/modify/run boards.txt.py to regenerate boards.txt. +# All modified files after running with option "--allgen" must be included in the pull-request. +# + +menu.UploadSpeed=Upload Speed +menu.CpuFrequency=CPU Frequency + +############################################################## +generic.name=Generic ESP8266 Module +generic.build.board=ESP8266_GENERIC +generic.upload.tool=esptool +generic.upload.maximum_data_size=81920 +generic.upload.wait_for_upload_port=true +generic.upload.erase_cmd= +generic.serial.disableDTR=true +generic.serial.disableRTS=true +generic.build.mcu=esp8266 +generic.build.core=esp8266 +generic.build.variant=generic +generic.build.spiffs_pagesize=256 +generic.build.debug_port= +generic.build.debug_level= +generic.menu.CpuFrequency.80=80 MHz +generic.menu.CpuFrequency.80.build.f_cpu=80000000L +generic.menu.CpuFrequency.160=160 MHz +generic.menu.CpuFrequency.160.build.f_cpu=160000000L +generic.menu.UploadSpeed.115200=115200 +generic.menu.UploadSpeed.115200.upload.speed=115200 +generic.menu.UploadSpeed.9600=9600 +generic.menu.UploadSpeed.9600.upload.speed=9600 +generic.menu.UploadSpeed.57600=57600 +generic.menu.UploadSpeed.57600.upload.speed=57600 +generic.menu.UploadSpeed.230400.linux=230400 +generic.menu.UploadSpeed.230400.macosx=230400 +generic.menu.UploadSpeed.230400.upload.speed=230400 +generic.menu.UploadSpeed.256000.windows=256000 +generic.menu.UploadSpeed.256000.upload.speed=256000 +generic.menu.UploadSpeed.460800.linux=460800 +generic.menu.UploadSpeed.460800.macosx=460800 +generic.menu.UploadSpeed.460800.upload.speed=460800 +generic.menu.UploadSpeed.512000.windows=512000 +generic.menu.UploadSpeed.512000.upload.speed=512000 +generic.menu.UploadSpeed.921600=921600 +generic.menu.UploadSpeed.921600.upload.speed=921600 + +############################################################## +wifi_slot.name=Amperka WiFi Slot +wifi_slot.build.board=AMPERKA_WIFI_SLOT +wifi_slot.build.variant=wifi_slot +wifi_slot.upload.tool=esptool +wifi_slot.upload.maximum_data_size=81920 +wifi_slot.upload.wait_for_upload_port=true +wifi_slot.upload.erase_cmd= +wifi_slot.serial.disableDTR=true +wifi_slot.serial.disableRTS=true +wifi_slot.build.mcu=esp8266 +wifi_slot.build.core=esp8266 +wifi_slot.build.spiffs_pagesize=256 +wifi_slot.build.debug_port= +wifi_slot.build.debug_level= +wifi_slot.menu.CpuFrequency.80=80 MHz +wifi_slot.menu.CpuFrequency.80.build.f_cpu=80000000L +wifi_slot.menu.CpuFrequency.160=160 MHz +wifi_slot.menu.CpuFrequency.160.build.f_cpu=160000000L +wifi_slot.menu.UploadSpeed.115200=115200 +wifi_slot.menu.UploadSpeed.115200.upload.speed=115200 +wifi_slot.menu.UploadSpeed.9600=9600 +wifi_slot.menu.UploadSpeed.9600.upload.speed=9600 +wifi_slot.menu.UploadSpeed.57600=57600 +wifi_slot.menu.UploadSpeed.57600.upload.speed=57600 +wifi_slot.menu.UploadSpeed.230400.linux=230400 +wifi_slot.menu.UploadSpeed.230400.macosx=230400 +wifi_slot.menu.UploadSpeed.230400.upload.speed=230400 +wifi_slot.menu.UploadSpeed.256000.windows=256000 +wifi_slot.menu.UploadSpeed.256000.upload.speed=256000 +wifi_slot.menu.UploadSpeed.460800.linux=460800 +wifi_slot.menu.UploadSpeed.460800.macosx=460800 +wifi_slot.menu.UploadSpeed.460800.upload.speed=460800 +wifi_slot.menu.UploadSpeed.512000.windows=512000 +wifi_slot.menu.UploadSpeed.512000.upload.speed=512000 +wifi_slot.menu.UploadSpeed.921600=921600 +wifi_slot.menu.UploadSpeed.921600.upload.speed=921600 diff --git a/packages/arduino-cli/test/mocha.opts b/packages/arduino-cli/test/mocha.opts index 38ee4bcbc..dc9fc30c5 100644 --- a/packages/arduino-cli/test/mocha.opts +++ b/packages/arduino-cli/test/mocha.opts @@ -1,3 +1,3 @@ --require babel-register --colors ---timeout 90000 +--timeout 120000 diff --git a/packages/arduino-cli/test/optionParser.spec.js b/packages/arduino-cli/test/optionParser.spec.js new file mode 100644 index 000000000..cd3b11a34 --- /dev/null +++ b/packages/arduino-cli/test/optionParser.spec.js @@ -0,0 +1,215 @@ +import path from 'path'; +import { assert } from 'chai'; +import fse from 'fs-extra'; + +import { + getBoardsTxtPath, + getLines, + parseOptionNames, + parseIntermediateOptions, + convertIntermediateOptions, + parseOptions, + patchBoardWithOptions, + patchBoardsWithOptions, +} from '../src/optionParser'; + +const fixtureDir = path.resolve(__dirname, 'fixtures'); + +// ============================================================================= +// +// Test data +// +// ============================================================================= + +const espOptionNames = { + UploadSpeed: 'Upload Speed', + CpuFrequency: 'CPU Frequency', +}; + +const espOptions = { + UploadSpeed: [ + { + name: '115200', + value: '115200', + }, + { + name: '9600', + value: '9600', + }, + { + name: '57600', + value: '57600', + }, + { + name: '921600', + value: '921600', + }, + ], + CpuFrequency: [ + { + name: '80 MHz', + value: '80', + }, + { + name: '160 MHz', + value: '160', + }, + ], +}; + +const uploadSpeedOptions = { + optionName: 'Upload Speed', + optionId: 'UploadSpeed', + values: [ + { + name: '115200', + value: '115200', + }, + { + name: '9600', + value: '9600', + }, + { + name: '57600', + value: '57600', + }, + { + name: '921600', + value: '921600', + }, + ], +}; +const cpuFrequencyOptions = { + optionName: 'CPU Frequency', + optionId: 'CpuFrequency', + values: [ + { + name: '80 MHz', + value: '80', + }, + { + name: '160 MHz', + value: '160', + }, + ], +}; + +const boards = [ + { + name: 'Generic ESP8266 Module', + fqbn: 'esp8266:esp8266:generic', + }, + { + name: 'Amperka WiFi Slot', + fqbn: 'esp8266:esp8266:wifi_slot', + }, + { + name: 'Arduino Due', + fqbn: 'arduino:sam:due', + }, + { + name: 'Arduino/Genuino Uno (not installed)', + package: 'arduino:avr', + packageName: 'Arduino AVR Boards', + version: '1.6.21', + }, +]; + +const expectedBoards = [ + { + name: 'Generic ESP8266 Module', + fqbn: 'esp8266:esp8266:generic', + options: [cpuFrequencyOptions, uploadSpeedOptions], + }, + { + name: 'Amperka WiFi Slot', + fqbn: 'esp8266:esp8266:wifi_slot', + options: [cpuFrequencyOptions, uploadSpeedOptions], + }, + { + name: 'Arduino Due', + fqbn: 'arduino:sam:due', + options: [], + }, + { + name: 'Arduino/Genuino Uno (not installed)', + package: 'arduino:avr', + packageName: 'Arduino AVR Boards', + version: '1.6.21', + }, +]; + +// ============================================================================= +// +// Specs +// +// ============================================================================= + +describe('Option Parser', () => { + let espBoardsTxtContent; + let espBoardsTxtLines; + before(async () => { + const buf = await fse.readFile( + getBoardsTxtPath(fixtureDir, 'esp8266:esp8266', '2.4.2') + ); + espBoardsTxtContent = buf.toString(); + espBoardsTxtLines = getLines(espBoardsTxtContent); + }); + + it('Parses human-readable option names', () => { + assert.deepEqual(parseOptionNames(espBoardsTxtLines), espOptionNames); + }); + + it('Parses options for each board into intermediate object', () => { + assert.deepEqual(parseIntermediateOptions(espBoardsTxtLines), { + generic: espOptions, + wifi_slot: espOptions, + }); + }); + + it('Converts parsed intermediate options into final Option list', () => { + assert.sameDeepMembers( + convertIntermediateOptions(espOptionNames, espOptions), + [uploadSpeedOptions, cpuFrequencyOptions] + ); + }); + + it('Parses options into final object, that should be merged into boards', () => { + const res = parseOptions(espBoardsTxtContent); + assert.hasAllKeys(res, ['generic', 'wifi_slot']); + assert.sameDeepMembers(res.generic, [ + uploadSpeedOptions, + cpuFrequencyOptions, + ]); + assert.sameDeepMembers(res.wifi_slot, [ + uploadSpeedOptions, + cpuFrequencyOptions, + ]); + }); + + it('Correctly patches Board objects with Options', () => { + assert.deepEqual( + patchBoardWithOptions(espBoardsTxtContent, boards[0]), + expectedBoards[0] + ); + assert.deepEqual( + patchBoardWithOptions(espBoardsTxtContent, boards[1]), + expectedBoards[1] + ); + assert.deepEqual( + patchBoardWithOptions(espBoardsTxtContent, boards[2]), + expectedBoards[2] + ); + assert.deepEqual( + patchBoardWithOptions(espBoardsTxtContent, boards[3]), + expectedBoards[3] + ); + }); + + it('Correctly loads boards.txt and patches all boards', () => + patchBoardsWithOptions( + fixtureDir, + [{ ID: 'esp8266:esp8266', Installed: '2.4.2' }], + boards + ).then(res => assert.sameDeepMembers(res, expectedBoards))); +}); diff --git a/packages/arduino-cli/test/parseTable.spec.js b/packages/arduino-cli/test/parseTable.spec.js deleted file mode 100644 index afec5407b..000000000 --- a/packages/arduino-cli/test/parseTable.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import { assert } from 'chai'; - -import parseTable from '../src/parseTable'; - -describe('parseTable()', () => { - it('returns array with valid JS objects', () => { - const stdout = `ID Installed Latest Name - arduino:avr 1.6.21 1.6.21 Arduino AVR Boards - esp8266:esp8266 2.4.2 2.4.2 esp8266 `; - - assert.deepEqual(parseTable(stdout), [ - { - ID: 'arduino:avr', - Installed: '1.6.21', - Latest: '1.6.21', - Name: 'Arduino AVR Boards', - }, - { - ID: 'esp8266:esp8266', - Installed: '2.4.2', - Latest: '2.4.2', - Name: 'esp8266', - }, - ]); - }); - it('returns empty array for empty source', () => { - assert.lengthOf(parseTable(''), 0); - }); - it('returns some shit for the broken data', () => { - const stdout = `ID Installed Latest Name - arduino:avr 1.6.21 - esp8266:esp8266 2.4.2 2.4.2 esp8266 some more and more `; - assert.deepEqual(parseTable(stdout), [ - { - ID: 'arduino:avr', - Installed: '1.6.21', - }, - { - ID: 'esp8266:esp8266', - Installed: '2.4.2', - Latest: '2.4.2', - Name: 'esp8266', - }, - ]); - }); -}); diff --git a/packages/xod-client-electron/src/app/arduinoCli.js b/packages/xod-client-electron/src/app/arduinoCli.js index f48bb4861..6b47b8233 100644 --- a/packages/xod-client-electron/src/app/arduinoCli.js +++ b/packages/xod-client-electron/src/app/arduinoCli.js @@ -20,6 +20,7 @@ import { compilationBegun, CODE_COMPILED, BEGIN_COMPILATION_IN_CLOUD, + UPLOAD_PROCESS_BEGINS, } from '../shared/messages'; import { ARDUINO_LIBRARIES_DIRNAME, @@ -135,6 +136,72 @@ const copyLibrariesToSketchbook = async cli => { return copyLibraries(bundledLibPath, userLibPath, sketchbookLibDir); }; +// :: Board -> Board +const patchFqbnWithOptions = board => { + const selectedOptions = board.selectedOptions || {}; + const options = board.options || []; + + const defaultBoardOptions = R.compose( + R.mergeAll, + R.reject(R.isNil), + R.map(opt => ({ + [opt.optionId]: R.pathOr(null, ['values', 0, 'value'], opt), + })) + )(options); + const defaultBoardOptionKeys = R.keys(defaultBoardOptions); + + // Find out selected board options that equal to default board options. + // + // TODO: + // It's better to use all options that was defined by User to be sure + // that will be compiled and uploaded as User desires, + // but arduino-cli@0.3.1 have a problem: + // https://github.com/arduino/arduino-cli/issues/64 + const equalToDefaultBoardOpionKeys = R.compose( + R.reduce( + (acc, [key, val]) => + defaultBoardOptions[key] && defaultBoardOptions[key] === val + ? R.append(key, acc) + : acc, + [] + ), + R.toPairs + )(selectedOptions); + + // Find out board option keys that does not fit the selected board + const staleBoardOptionKeys = R.compose( + R.reject(isAmong(defaultBoardOptionKeys)), + R.keys + )(selectedOptions); + + const keysToOmit = R.concat( + equalToDefaultBoardOpionKeys, + staleBoardOptionKeys + ); + + // TODO + // This is a kludge cause arduino-cli 0.3.1 + // can't find out all default board options. + // So we have to specify at least one option. + const oneOfDefaultOptions = R.compose( + R.pick(R.__, defaultBoardOptions), + R.of, + R.head + )(defaultBoardOptionKeys); + + const selectedBoardOptions = R.omit(keysToOmit, selectedOptions); + + return R.compose( + R.assoc('fqbn', R.__, board), + R.concat(board.fqbn), + R.unless(R.isEmpty, R.concat(':')), + R.join(','), + R.map(R.join('=')), + R.toPairs, + R.when(R.isEmpty, R.always(oneOfDefaultOptions)) + )(selectedBoardOptions); +}; + // ============================================================================= // // Handlers @@ -290,7 +357,7 @@ const uploadThroughCloud = async (onProgress, cli, payload) => { const uploadLog = await cli.upload( stdout => onProgress({ - percentage: 100, + percentage: 60, message: stdout, tab: 'uploader', }), @@ -346,10 +413,16 @@ const uploadThroughUSB = async (onProgress, cli, payload) => { false ); + onProgress({ + percentage: 50, + message: UPLOAD_PROCESS_BEGINS, + tab: 'uploader', + }); + const uploadLog = await cli.upload( stdout => onProgress({ - percentage: 100, + percentage: 60, message: stdout, tab: 'uploader', }), @@ -378,7 +451,12 @@ const uploadThroughUSB = async (onProgress, cli, payload) => { */ export const upload = (onProgress, cli, payload) => { const uploadFn = payload.cloud ? uploadThroughCloud : uploadThroughUSB; - return uploadFn(onProgress, cli, payload); + const payloadWithUpdatedFqbn = R.over( + R.lensProp('board'), + patchFqbnWithOptions, + payload + ); + return uploadFn(onProgress, cli, payloadWithUpdatedFqbn); }; // ============================================================================= diff --git a/packages/xod-client-electron/src/app/constants.js b/packages/xod-client-electron/src/app/constants.js index 2639564a3..c7bba47eb 100644 --- a/packages/xod-client-electron/src/app/constants.js +++ b/packages/xod-client-electron/src/app/constants.js @@ -1,6 +1,10 @@ export const ARDUINO_PACKAGES_DIRNAME = '__packages__'; export const BUNDLED_ADDITIONAL_URLS = [ - 'http://arduino.esp8266.com/stable/package_esp8266com_index.json', + // TODO: + // Replace our fixed esp8266 package with original: + // http://arduino.esp8266.com/stable/package_esp8266com_index.json + // When they release new version >2.4.2 + 'https://storage.googleapis.com/releases.xod.io/packages/esp8266-2.4.3/package_esp8266com_index.json', ]; export const ARDUINO_LIBRARIES_DIRNAME = '__ardulib__'; export const ARDUINO_CLI_LIBRARIES_DIRNAME = 'libraries'; diff --git a/packages/xod-client-electron/src/arduinoDependencies/messages.js b/packages/xod-client-electron/src/arduinoDependencies/messages.js index 163171bc9..f9506df8b 100644 --- a/packages/xod-client-electron/src/arduinoDependencies/messages.js +++ b/packages/xod-client-electron/src/arduinoDependencies/messages.js @@ -3,14 +3,18 @@ const librariesMissing = libraryNames => ? `You have to install these libraries first: ${libraryNames}` : null; const librariesInstalled = libraryNames => - libraryNames.length ? `Libraries ${libraryNames} installed` : null; + libraryNames.length + ? `Libraries ${libraryNames} installed successfully` + : null; const packagesMissing = packageNames => packageNames.length ? `You have to install these packages first: ${packageNames}` : null; const packagesInstalled = packageNames => - packageNames.length ? `Packages ${packageNames} installed` : null; + packageNames.length + ? `Package "${packageNames}" installed successfully` + : null; export default { ARDUINO_DEPENDENCIES_MISSING: ({ @@ -31,6 +35,7 @@ export default { note: [librariesInstalled(libraryNames), packagesInstalled(packageNames)] .filter(a => !!a) .join('\r\n'), + solution: 'Now you are able to upload the program', persistent: true, }), }; diff --git a/packages/xod-client-electron/src/shared/messages.js b/packages/xod-client-electron/src/shared/messages.js index f3972cbae..a48799f9c 100644 --- a/packages/xod-client-electron/src/shared/messages.js +++ b/packages/xod-client-electron/src/shared/messages.js @@ -28,6 +28,8 @@ export const DEBUG_SESSION_STOPPED_ON_TAB_CLOSE = { }; export const DEBUG_LOST_CONNECTION = 'Lost connection with the device.'; +export const UPLOAD_PROCESS_BEGINS = 'Uploading compiled code to the board...'; + export const updateAvailableMessage = version => composeMessage( 'Update available', diff --git a/packages/xod-client-electron/src/upload/components/PopupUploadConfig.jsx b/packages/xod-client-electron/src/upload/components/PopupUploadConfig.jsx index c25fb56a5..39b6c9b72 100644 --- a/packages/xod-client-electron/src/upload/components/PopupUploadConfig.jsx +++ b/packages/xod-client-electron/src/upload/components/PopupUploadConfig.jsx @@ -11,20 +11,13 @@ import { } from '../../shared/messages'; import { updateIndexFiles } from '../arduinoCli'; -// :: Board -> Boolean -const hasBoardCpu = board => - board.cpuName && - board.cpuName.length > 0 && - board.cpuId && - board.cpuId.length > 0; - class PopupUploadConfig extends React.Component { constructor(props) { super(props); this.state = { isVisible: props.isVisible, - selectedBoard: null, + selectedBoard: null, // or { index: Number, options: Map OptionId OptionValue } boards: null, ports: null, doCompileInCloud: false, @@ -40,6 +33,7 @@ class PopupUploadConfig extends React.Component { this.onDebugCheckboxChanged = this.onDebugCheckboxChanged.bind(this); this.changeBoard = this.changeBoard.bind(this); + this.changeBoardOption = this.changeBoardOption.bind(this); this.changePort = this.changePort.bind(this); this.updateIndexes = this.updateIndexes.bind(this); @@ -74,8 +68,16 @@ class PopupUploadConfig extends React.Component { } onUploadClicked() { + const selectedBoard = this.state.selectedBoard; + const originalBoardData = this.state.boards[selectedBoard.index]; + const boardToUpload = R.assoc( + 'selectedOptions', + selectedBoard.options, + originalBoardData + ); + this.props.onUpload( - this.state.selectedBoard, + boardToUpload, this.props.selectedPort, this.state.doCompileInCloud, this.state.debugAfterUpload @@ -112,7 +114,8 @@ class PopupUploadConfig extends React.Component { .then(R.tap(boards => this.setState({ boards }))) .then(boards => { const doesSelectedBoardExist = - isBoardSelected && R.contains(selectedBoard, boards); + isBoardSelected && boards[selectedBoard.index]; + const defaultBoardIndex = R.compose( R.defaultTo(0), R.findIndex(R.propEq('fqbn', 'arduino:avr:uno')) @@ -156,11 +159,7 @@ class PopupUploadConfig extends React.Component { } getSelectedBoardIndex() { - return R.compose( - R.when(R.equals(-1), R.always(0)), - R.findIndex(R.equals(this.state.selectedBoard)), - R.defaultTo([]) - )(this.state.boards); + return R.pathOr(0, ['selectedBoard', 'index'], this.state); } getSelectedPortName() { @@ -170,7 +169,7 @@ class PopupUploadConfig extends React.Component { getSelectedBoard() { return this.props .getSelectedBoard() - .then(R.tap(board => this.setState({ selectedBoard: board }))); + .then(R.tap(selBoard => this.setState({ selectedBoard: selBoard }))); } updateIndexes() { @@ -182,11 +181,19 @@ class PopupUploadConfig extends React.Component { } changeBoard(boardIndex) { - if (this.state.boards) { - const board = this.state.boards[boardIndex] || this.state.boards[0]; - this.props.onBoardChanged(board); - this.setState({ selectedBoard: board }); - } + const newBoard = R.assoc('index', boardIndex, this.state.selectedBoard); + this.props.onBoardChanged(newBoard); + this.setState({ selectedBoard: newBoard }); + } + + changeBoardOption(optionId, optionValue) { + const newBoard = R.over( + R.lensProp('options'), + R.assoc(optionId, optionValue), + this.state.selectedBoard + ); + this.props.onBoardChanged(newBoard); + this.setState({ selectedBoard: newBoard }); } changePort(port) { @@ -194,7 +201,11 @@ class PopupUploadConfig extends React.Component { } canUnpload() { - return this.state.selectedBoard && this.props.selectedPort; + return ( + this.state.selectedBoard && + this.props.selectedPort && + !this.props.isDeploymentInProgress + ); } renderBoardSelect() { @@ -217,7 +228,6 @@ class PopupUploadConfig extends React.Component { {this.state.boards.map((board, ix) => ( ))} @@ -247,6 +257,47 @@ class PopupUploadConfig extends React.Component { ); } + renderBoardOptions() { + const selectedBoard = this.state.selectedBoard; + if (!selectedBoard || !this.state.boards) return null; + + const board = this.state.boards[selectedBoard.index]; + const options = R.propOr([], 'options', board); + if (R.isEmpty(options)) return null; + + return ( +
+ {R.map( + opt => ( +
+ + +
+ ), + options + )} +
+ ); + } + renderPortSelect() { const isSelecting = this.state.ports === null; const hasPorts = this.state.ports !== null && this.state.ports.length > 0; @@ -298,6 +349,7 @@ class PopupUploadConfig extends React.Component { render() { const boards = this.renderBoardSelect(); + const boardOptions = this.renderBoardOptions(); const ports = this.renderPortSelect(); const compileLimitLeft = this.props.compileLimitLeft; @@ -307,7 +359,10 @@ class PopupUploadConfig extends React.Component { title="Upload project to Arduino" onClose={this.onClose} > -
{boards}
+
+ {boards} + {boardOptions} +
{ports}
Upload + {this.props.isDeploymentInProgress ? ( + + Another deployment job is in progress + + ) : null}
); @@ -351,6 +411,7 @@ class PopupUploadConfig extends React.Component { PopupUploadConfig.propTypes = { isVisible: PropTypes.bool, + isDeploymentInProgress: PropTypes.bool, initialDebugAfterUpload: PropTypes.bool, selectedPort: PropTypes.object, compileLimitLeft: PropTypes.number, @@ -366,6 +427,7 @@ PopupUploadConfig.propTypes = { PopupUploadConfig.defaultProps = { isVisible: false, + isDeploymentInProgress: false, initialDebugAfterUpload: false, }; diff --git a/packages/xod-client-electron/src/upload/messages.js b/packages/xod-client-electron/src/upload/messages.js index e30c574e3..c4a58414e 100644 --- a/packages/xod-client-electron/src/upload/messages.js +++ b/packages/xod-client-electron/src/upload/messages.js @@ -4,4 +4,10 @@ export default { note: `Cloud compilation does not support ${boardName} yet.`, solution: 'Try to compile it on your own computer', }), + UPLOAD_TOOL_ERROR: ({ message }) => ({ + title: 'Upload tool exited with error', + note: `Command ${message}`, + solution: + 'Make sure the board is connected, the cable is working, the board model set correctly, the upload port belongs to the board, the board drivers are installed, the upload options (if any) match your board specs.', + }), }; diff --git a/packages/xod-client-electron/src/upload/reducer.js b/packages/xod-client-electron/src/upload/reducer.js index ea2e10cb4..b07468640 100644 --- a/packages/xod-client-electron/src/upload/reducer.js +++ b/packages/xod-client-electron/src/upload/reducer.js @@ -1,8 +1,24 @@ import * as R from 'ramda'; -import { SELECT_SERIAL_PORT } from '../upload/actionTypes'; +import client from 'xod-client'; +import { SELECT_SERIAL_PORT, UPLOAD } from '../upload/actionTypes'; const initialState = { selectedSerialPort: null, + isUploading: false, + isInstallingArduinoDependencies: false, +}; + +const switchProcess = (propName, action, state) => { + const status = R.path(['meta', 'status'], action); + + if (status === client.STATUS.STARTED) { + return R.assoc(propName, true, state); + } + if (status === client.STATUS.SUCCEEDED || status === client.STATUS.FAILED) { + return R.assoc(propName, false, state); + } + + return state; }; export default (state = initialState, action) => { @@ -10,6 +26,12 @@ export default (state = initialState, action) => { case SELECT_SERIAL_PORT: return R.assoc('selectedSerialPort', action.payload.port, state); + case UPLOAD: + return switchProcess('isUploading', action, state); + + case client.INSTALL_ARDUINO_DEPENDENCIES: + return switchProcess('isInstallingArduinoDependencies', action, state); + default: return state; } diff --git a/packages/xod-client-electron/src/upload/selectors.js b/packages/xod-client-electron/src/upload/selectors.js index bff02b30a..91bbed5d0 100644 --- a/packages/xod-client-electron/src/upload/selectors.js +++ b/packages/xod-client-electron/src/upload/selectors.js @@ -29,6 +29,14 @@ export const getSelectedSerialPort = R.compose( getUploadState ); +export const isDeploymentInProgress = R.compose( + R.converge(R.or, [ + R.prop('isUploading'), + R.prop('isInstallingArduinoDependencies'), + ]), + getUploadState +); + export default { getUploadProcess, }; diff --git a/packages/xod-client-electron/src/view/containers/App.jsx b/packages/xod-client-electron/src/view/containers/App.jsx index 87259370b..dd59e2aed 100644 --- a/packages/xod-client-electron/src/view/containers/App.jsx +++ b/packages/xod-client-electron/src/view/containers/App.jsx @@ -31,6 +31,7 @@ import { listBoards, upload } from '../../upload/arduinoCli'; import * as debuggerIPC from '../../debugger/ipcActions'; import { getUploadProcess, + isDeploymentInProgress, getSelectedSerialPort, } from '../../upload/selectors'; import * as settingsActions from '../../settings/actions'; @@ -271,6 +272,8 @@ class App extends client.App { logProcessFn(messageForConsole, 0); }; + stopDebuggerSession(); + eitherToPromise(eitherTProject) .then( tapP(tProj => { @@ -288,7 +291,6 @@ class App extends client.App { R.always({ packages: [] }) ) )(board); - // TODO: Add other arduino dependencies here return checkArduinoDependencies( progressData => checkProcess.progress( @@ -321,7 +323,15 @@ class App extends client.App { board, port, } - ) + ).catch(err => { + console.error(err); // eslint-disable-line no-console + return Promise.reject( + createError('UPLOAD_TOOL_ERROR', { + message: err.message, + code: err.code, + }) + ); + }) ) .then(() => proc.success()) .then(() => { @@ -776,6 +786,7 @@ class App extends client.App { return this.props.popups.uploadToArduinoConfig ? (