├── .nvmrc ├── .gitignore ├── demo ├── .babelrc ├── index.js ├── v2 │ ├── v2.jsx │ └── install_tool.jsx ├── index.html ├── serial_mirror.js └── app.jsx ├── .editorconfig ├── .babelrc ├── .eslintrc ├── webpack.config.js ├── rollup.config.js ├── src ├── index.js ├── socket-daemon.v2.js ├── chrome-os-daemon.js ├── signatures.js ├── chrome-app-daemon.js ├── firmware-updater.js ├── web-serial-daemon.js ├── daemon.js └── socket-daemon.js ├── CHANGELOG.md ├── package.json ├── README.md └── LICENSE.txt /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.16.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | dist 4 | es 5 | .tmp 6 | misc 7 | -------------------------------------------------------------------------------- /demo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env", 4 | "@babel/react" 5 | ] 6 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": { 4 | "presets": [ 5 | [ 6 | "@babel/env", 7 | { 8 | "useBuiltIns": false 9 | } 10 | ] 11 | ] 12 | }, 13 | "es": { 14 | "presets": [ 15 | [ 16 | "@babel/env", 17 | { 18 | "useBuiltIns": false, 19 | "modules": false 20 | } 21 | ] 22 | ] 23 | } 24 | }, 25 | "plugins": [ 26 | "@babel/proposal-object-rest-spread" 27 | ] 28 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "rules": { 7 | "max-len": 0, 8 | "comma-dangle": 0, 9 | "brace-style": [2, "stroustrup"], 10 | "no-console": 0, 11 | "padded-blocks": 0, 12 | "indent": [2, 2, {"SwitchCase": 1}], 13 | "spaced-comment": 1, 14 | "quotes": ["error", "single", { "allowTemplateLiterals": true }], 15 | "import/prefer-default-export": "off", 16 | "arrow-parens": 0, 17 | "consistent-return": 0, 18 | "no-useless-escape": 0, 19 | "no-underscore-dangle": 0, 20 | "react/jsx-uses-vars": 2 21 | }, 22 | "extends": "airbnb-base", 23 | "env": { 24 | "browser": true, 25 | "jest": true, 26 | "webextensions": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | const src = path.join(__dirname, 'demo'); 5 | const dist = path.join(__dirname, '.tmp'); 6 | 7 | module.exports = { 8 | entry: { 9 | app: path.join(src, './index.js') 10 | }, 11 | devtool: 'inline-source-map', 12 | devServer: { 13 | contentBase: dist, 14 | port: 8000, 15 | allowedHosts: ['local.arduino.cc'] 16 | }, 17 | plugins: [ 18 | new HtmlWebpackPlugin({ 19 | template: path.join(src, 'index.html') 20 | }) 21 | ], 22 | output: { 23 | filename: '[name].bundle.js', 24 | path: dist 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.(js|jsx)$/, 30 | exclude: /(node_modules)/, 31 | use: { 32 | loader: 'babel-loader', 33 | options: { 34 | presets: [ 35 | '@babel/preset-env', 36 | { plugins: ['@babel/plugin-proposal-class-properties'] } 37 | ] 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import babel from 'rollup-plugin-babel'; 4 | import replace from 'rollup-plugin-replace'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | 7 | const env = process.env.NODE_ENV; 8 | const isProduction = env === 'production'; 9 | 10 | export default { 11 | output: { 12 | format: 'umd', 13 | name: 'CreatePlugin', 14 | }, 15 | plugins: [ 16 | nodeResolve(), 17 | // due to https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module 18 | commonjs({ 19 | include: 'node_modules/**', 20 | namedExports: { './node_module/invariant.js': ['default'] } 21 | }), 22 | babel({ 23 | exclude: 'node_modules/**' 24 | }), 25 | replace({ 26 | 'process.env.NODE_ENV': JSON.stringify(env) 27 | }), 28 | isProduction && terser({ 29 | compress: { 30 | pure_getters: true, 31 | unsafe: true, 32 | unsafe_comps: true, 33 | warnings: false 34 | } 35 | }) 36 | ] 37 | }; 38 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 ARDUINO SA (http://www.arduino.cc/) 3 | * This file is part of arduino-create-agent-js-client. 4 | * Copyright (c) 2018 5 | * Authors: Alberto Iannaccone, Stefania Mellai, Gabriele Destefanis 6 | * 7 | * This software is released under: 8 | * The GNU General Public License, which covers the main part of 9 | * arduino-create-agent-js-client 10 | * The terms of this license can be found at: 11 | * https://www.gnu.org/licenses/gpl-3.0.en.html 12 | * 13 | * You can be released from the requirements of the above licenses by purchasing 14 | * a commercial license. Buying such a license is mandatory if you want to modify or 15 | * otherwise use the software for commercial activities involving the Arduino 16 | * software without disclosing the source code of your own applications. To purchase 17 | * a commercial license, send an email to license@arduino.cc. 18 | * 19 | */ 20 | 21 | import React from 'react'; 22 | import ReactDOM from 'react-dom'; 23 | 24 | import App from './app.jsx'; 25 | 26 | // Mounts the App component into the
element in the index.html 27 | ReactDOM.render(React.createElement(App, null, null), document.getElementById('root')); 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 ARDUINO SA (http://www.arduino.cc/) 3 | * This file is part of arduino-create-agent-js-client. 4 | * Copyright (c) 2018 5 | * Authors: Alberto Iannaccone, Stefania Mellai, Gabriele Destefanis 6 | * 7 | * This software is released under: 8 | * The GNU General Public License, which covers the main part of 9 | * arduino-create-agent-js-client 10 | * The terms of this license can be found at: 11 | * https://www.gnu.org/licenses/gpl-3.0.en.html 12 | * 13 | * You can be released from the requirements of the above licenses by purchasing 14 | * a commercial license. Buying such a license is mandatory if you want to modify or 15 | * otherwise use the software for commercial activities involving the Arduino 16 | * software without disclosing the source code of your own applications. To purchase 17 | * a commercial license, send an email to license@arduino.cc. 18 | * 19 | */ 20 | 21 | import SocketDaemon from './socket-daemon'; 22 | import ChromeOsDaemon from './chrome-os-daemon'; 23 | import FirmwareUpdater from './firmware-updater'; 24 | 25 | const Daemon = window.navigator.userAgent.indexOf(' CrOS ') !== -1 ? ChromeOsDaemon : SocketDaemon; 26 | 27 | export default Daemon; 28 | export { FirmwareUpdater }; 29 | -------------------------------------------------------------------------------- /demo/v2/v2.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { V2InstallTool } from './install_tool.jsx'; 3 | 4 | class V2 extends React.Component { 5 | constructor() { 6 | super(); 7 | this.state = { 8 | tools: [] 9 | }; 10 | } 11 | 12 | componentDidMount() { 13 | this.daemon = this.props.daemon; 14 | 15 | if (this.daemon.agentV2Found) { // agentV2Found not available for instance on chromebooks 16 | this.daemon.agentV2Found.subscribe(daemonV2 => { 17 | if (!daemonV2) { 18 | return; 19 | } 20 | this.daemonV2 = daemonV2; 21 | this.daemonV2.installedTools().then(res => { 22 | this.setState({ 23 | tools: res 24 | }); 25 | }); 26 | }); 27 | } 28 | } 29 | 30 | render() { 31 | const tools = this.state.tools.map((tool, i) => 32 | {tool.packager} 33 | {tool.name} 34 | {tool.version} 35 | ); 36 | 37 | return ( 38 |
39 |

V2

40 |
41 |

Installed tools

42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {tools} 52 |
PackagerNameVersion
53 |
54 | 55 | 56 |
57 |
58 | ); 59 | } 60 | } 61 | 62 | export default V2; 63 | -------------------------------------------------------------------------------- /src/socket-daemon.v2.js: -------------------------------------------------------------------------------- 1 | export default class SocketDaemonV2 { 2 | constructor(daemonURL) { 3 | this.daemonURL = `${daemonURL}/v2`; 4 | } 5 | 6 | // init tries an HEAD 7 | init() { 8 | return fetch(`${this.daemonURL}/pkgs/tools/installed`, { 9 | method: 'HEAD', 10 | }).then(res => { 11 | if (res.status !== 200) { 12 | throw Error('v2 not available'); 13 | } 14 | return res; 15 | }); 16 | } 17 | 18 | // installedTools uses the new v2 apis to ask the daemon a list of the tools already present in the system 19 | installedTools() { 20 | return fetch(`${this.daemonURL}/pkgs/tools/installed`, { 21 | method: 'GET', 22 | }).then(res => res.json()); 23 | } 24 | 25 | // installTool uses the new v2 apis to ask the daemon to download a specific tool on the system 26 | // The expected payload is 27 | // { 28 | // "name": "avrdude", 29 | // "version": "6.3.0-arduino9", 30 | // "packager": "arduino", 31 | // "url": "https://downloads.arduino.cc/...", // system-specific package containing the tool 32 | // "signature": "e7Gh8309...", // proof that the url comes from a trusted source 33 | // "checksum": "SHA256:90384nhfoso8..." // proof that the package wasn't tampered with 34 | // } 35 | installTool(payload) { 36 | return fetch(`${this.daemonURL}/pkgs/tools/installed`, { 37 | method: 'POST', 38 | body: JSON.stringify(payload) 39 | }).then(res => res.json() 40 | .then((json) => { 41 | if (!res.ok) { 42 | const error = { 43 | ...json, 44 | status: res.status, 45 | statusText: res.statusText, 46 | }; 47 | return Promise.reject(error); 48 | } 49 | return json; 50 | })); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [2.11.0] - 2022-09-27 5 | The main goal of this release is to improve support for the Web Serial API on ChromeOS. 6 | Other platforms should not be affected. 7 | 8 | ### Changed 9 | - When using Web Serial API, the interactions between the client library 10 | (as an example, the Arduino `arduino-chromeos-uploader` libray) has been simplified. 11 | - A new parameter `dialogCustomizations` has been added to the upload functionality. It's used 12 | to provide custom confirmation dialogs when using the Web Serial API. 13 | It has no effect with other daemons. 14 | 15 | ### Removed 16 | - `cdcReset` functionality, now it's embedded in the `upload` functionality 17 | in the Web Serial daemon. 18 | ### Changed 19 | 20 | ## [2.10.1] - 2022-09-08 21 | 22 | ### Changed 23 | - Fixed a bug released in 2.9.1 caused by the wrong assumption that the build filename is always at the end of the command line. This fix makes the library backward compatible with older ESP boards. 24 | 25 | ## *DEPRECATED* [2.9.1] - 2022-09-06 26 | ### Added 27 | - Added support for ESP32 boards 28 | 29 | ## [2.9.0] - 2022-06-06 30 | ### Added 31 | - Added support for "Arduino RP2040 Connect" board 32 | ### Changed 33 | - Improved support for Chrome's Web Serial API on ChromeOS. Other operating systems should not be affected. 34 | - Simplified the communication with the Web Serial API via a messaging system which simulates 35 | the [postMessage](https://developer.chrome.com/docs/extensions/reference/runtime/#method-Port-postMessage) function available in the Chrome App Daemon (see `chrome-app-daemon.js`). 36 | 37 | ## [2.8.0] - 2022-03-21 38 | ### Added 39 | - Added support (still in Beta) for Chrome's Web Serial API on ChromeOS. 40 | Other operating systems should not be affected. 41 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Arduino Create Plugin Client 26 | 84 | 85 | 86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /src/chrome-os-daemon.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 ARDUINO SA (http://www.arduino.cc/) 3 | * This file is part of arduino-create-agent-js-client. 4 | * Copyright (c) 2018 5 | * Authors: Alberto Iannaccone, Stefania Mellai, Gabriele Destefanis 6 | * 7 | * This software is released under: 8 | * The GNU General Public License, which covers the main part of 9 | * arduino-create-agent-js-client 10 | * The terms of this license can be found at: 11 | * https://www.gnu.org/licenses/gpl-3.0.en.html 12 | * 13 | * You can be released from the requirements of the above licenses by purchasing 14 | * a commercial license. Buying such a license is mandatory if you want to modify or 15 | * otherwise use the software for commercial activities involving the Arduino 16 | * software without disclosing the source code of your own applications. To purchase 17 | * a commercial license, send an email to license@arduino.cc. 18 | * 19 | */ 20 | import WebSerialDaemon from './web-serial-daemon'; 21 | import ChromeAppDaemon from './chrome-app-daemon'; 22 | 23 | /** 24 | * ChromeOSDaemon is a new implementation for ChromeOS which allows 25 | + to select the legacy Chrome app or the new BETA web serial API, 26 | * based on the the existance of a `useWebSerial` key available in the constructor. 27 | * Warning: support for WebSerialDaemon is still in alpha, so if you don't know 28 | * how to deal with Web Serial API, just stick with the Chrome App Deamon. 29 | * 30 | */ 31 | export default function ChromeOsDaemon(boardsUrl, options) { 32 | 33 | let useWebSerial; 34 | let chromeExtensionId; 35 | let uploader; 36 | 37 | // check chromeExtensionId OR web serial API 38 | if (typeof options === 'string') { 39 | chromeExtensionId = options; 40 | } 41 | else { 42 | chromeExtensionId = options.chromeExtensionId; 43 | useWebSerial = options.useWebSerial; 44 | uploader = options.uploader; 45 | } 46 | 47 | if ('serial' in navigator && useWebSerial && Boolean(uploader)) { 48 | console.debug('Instantiating WebSerialDaemon'); 49 | this.flavour = new WebSerialDaemon(boardsUrl, uploader); 50 | } 51 | else { 52 | console.debug('Instantiating ChromeAppDaemon'); 53 | this.flavour = new ChromeAppDaemon(boardsUrl, chromeExtensionId); 54 | } 55 | 56 | const handler = { 57 | get: (_, name) => this.flavour[name], 58 | 59 | set: (_, name, value) => { 60 | this.flavour[name] = value; 61 | return true; 62 | } 63 | }; 64 | 65 | return new Proxy(this, handler); 66 | 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arduino-create-agent-js-client", 3 | "version": "2.15.1", 4 | "description": "JS module providing discovery of the Arduino Create Plugin and communication with it", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "jsnext:main": "es/index.js", 8 | "files": [ 9 | "dist", 10 | "lib", 11 | "es", 12 | "src" 13 | ], 14 | "directories": { 15 | "lib": "lib" 16 | }, 17 | "dependencies": { 18 | "detect-browser": "^4.8.0", 19 | "rxjs": "^6.5.3", 20 | "semver-compare": "^1.0.0", 21 | "socket.io-client": "2.3.0" 22 | }, 23 | "devDependencies": { 24 | "@babel/cli": "^7.12.8", 25 | "@babel/core": "^7.12.9", 26 | "@babel/plugin-proposal-class-properties": "^7.16.7", 27 | "@babel/plugin-proposal-object-rest-spread": "7.12.13", 28 | "@babel/preset-env": "^7.12.7", 29 | "@babel/preset-es2015": "^7.0.0-beta.53", 30 | "@babel/preset-react": "^7.12.7", 31 | "babel-eslint": "^10.0.3", 32 | "babel-loader": "^8.0.6", 33 | "cross-env": "^7.0.3", 34 | "eslint": "^7.6.0", 35 | "eslint-config-airbnb-base": "^14.0.0", 36 | "eslint-plugin-import": "^2.18.2", 37 | "eslint-plugin-react": "^7.21.5", 38 | "html-webpack-plugin": "^3.2.0", 39 | "lodash": "^4.17.20", 40 | "react": "^17.0.1", 41 | "react-dom": "^17.0.1", 42 | "rimraf": "^3.0.0", 43 | "rollup": "^1.27.0", 44 | "rollup-plugin-babel": "^4.3.3", 45 | "rollup-plugin-commonjs": "^10.1.0", 46 | "rollup-plugin-node-resolve": "^5.2.0", 47 | "rollup-plugin-replace": "^2.2.0", 48 | "rollup-plugin-terser": "^4.0.4", 49 | "rollup-watch": "^4.3.1", 50 | "webpack": "^4.41.2", 51 | "webpack-cli": "^3.3.10", 52 | "webpack-dev-server": "^3.11.0" 53 | }, 54 | "scripts": { 55 | "test": "", 56 | "https": "webpack-dev-server --https", 57 | "dev": "webpack-dev-server", 58 | "lint": "./node_modules/.bin/eslint src", 59 | "lint-fix": "./node_modules/.bin/eslint --fix src --ext .js", 60 | "clean": "rimraf lib dist es", 61 | "build": "npm run build:commonjs && npm run build:umd && npm run build:umd:min && npm run build:es", 62 | "build:watch": "echo 'build && watch the COMMONJS version of the package - for other version, run specific tasks' && npm run build:commonjs:watch", 63 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 64 | "build:commonjs:watch": "npm run build:commonjs -- --watch", 65 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 66 | "build:es:watch": "npm run build:es -- --watch", 67 | "build:umd": "cross-env BABEL_ENV=es NODE_ENV=development node_modules/.bin/rollup src/index.js --config --sourcemap --file dist/create-plugin.js", 68 | "build:umd:watch": "npm run build:umd -- --watch", 69 | "build:umd:min": "cross-env BABEL_ENV=es NODE_ENV=production rollup src/index.js --config --file dist/create-plugin.min.js", 70 | "prepare": "npm run clean && npm test && npm run build" 71 | }, 72 | "repository": { 73 | "type": "git", 74 | "url": "git+https://github.com/arduino/arduino-create-agent-js-client.git" 75 | }, 76 | "keywords": [ 77 | "arduino", 78 | "create", 79 | "agent", 80 | "plugin" 81 | ], 82 | "contributors": [ 83 | "Stefania Mellai ", 84 | "Fabrizio Mirabito ", 85 | "Alberto Iannaccone ", 86 | "Gabriele Destefanis ", 87 | "Christian Sarnataro " 88 | ], 89 | "license": "GPLv3", 90 | "bugs": { 91 | "url": "https://github.com/arduino/arduino-create-agent-js-client/issues" 92 | }, 93 | "homepage": "https://github.com/arduino/arduino-create-agent-js-client#readme" 94 | } 95 | -------------------------------------------------------------------------------- /src/signatures.js: -------------------------------------------------------------------------------- 1 | // For versions of the fwupater tool >= 0.1.2 2 | export const fwupdaterSignatures = Object.freeze({ 3 | GET_FIRMWARE_INFO: '5aae0c2b9cfa783ab6d791491b6ebcb7ffb69644dcc8984def2a5f69029a46701dc8c95fc38a60efebb78c2802b6d172d7f38852b44ae1d2697c0211b6ac389f574c4ff85593ccae55e7c8c415f8d07f932fc64aec620ddb925dbd97b77cf395b9929911b6d4c40b8d3d5a4720a0613fe301344a45f505a430c956a527831896b42fddd0f737d630a3dc3714ce421bd30e9229b2ed503667a915bfc696b6221759640ff492e37356ef025ba9802d578227b8f15fd0f647c395bf73a84adf7a57281c31bc743ca92c09f8eec64d428acd25ced8f8420fdbf989db3625662970d3f16693fec44a418e8c7b12e9f4e94d353e4d6f6876bd74fc543eaeba20a09dc6', 4 | UPLOAD_FIRMWARE_BOSSAC: 'a965a14ec42c35a67f8d2f3f5235aaacbcc1b998b367d07a02d12992ec691f1723d00f0c3c1c64566e10823cca4947d7dd91e9ba4b28dffc112953f4ccf230adca274133f14ab3a13697c932c6c19ba05b4cd6d93141a31977ce7a53b253d761e2a503f55599a03605264d32e31c938966041709a824e64ddb0a2371e68c8d298dcd93cc8d451b8df1a7c1244e30b310fb10ec13448df3238bd0db5be0d1ec938b82b7c4f81262155c75b805700d0ff20b0404e27142014a2d74c3db677f32854dcda9052ccad9028e16cfb5151c09763e93a3a285be226dac849c519d79301962488c92723781d6a2063ee0021fa9f2c8d2dfcb7a6f01bcb4dbcd6b9f0d6322', 5 | UPLOAD_FIRMWARE_AVRDUDE: '67e99fc73b0c9359219d30c11851d728a5e5559426c0151518aa947b48af3f58de4d0997143cad18bc1a2f6ae22863baab2e8b0de6fff549185f3dc82cbcce44690e0c50a92cebf0bd4377ae85f3961330c5fda40f3a567242eb74fe3d937b1abfe8be77a9fd658eed1a765bbc264702577fdd63da8d3073dd36dcdd37dc5762b162e18c284715012c1f303c77475703a5c8ef8bfc685e1650eb26058016d771bd4f53929a51666aef70126aabda80431671eb2ba9b729ee166079abac806567b9ae63ce2801f88964250596b8450fda112ac43b868634b91cb59d91d13c286d3ec19eb4a840d3af89413d24b198e2da616cb2bbff45cc58645aa1b1d52f71bf', 6 | UPLOAD_FIRMWARE_RP2040: '422df04bfb4fc0eea4df716b3d9b075558c6094a2953795cf0f9cfdc58e97f9df80f51f8981d11e23a17fe1c220a164a87e24e1b10442cb6e06bda96737f1c30d7ef5928f4d926428a36ff4ea7d6cd379d2d755cda76f1fc9884628da519197007f9db1233401af387a6684f2a74855168f878d8c67239d99fdf7597379650cd9df6a56c430dc09ccc14fec3ceba56b9a0ab1e7b0a71cb9c9016ee3595a7d914e14d9f333a388aca31093ec77f347ceb0ed3f1d57861c7304bf53e45188ce3a976d294afd1e919496856b760dc708cb2db5cde585c44e8f023ad3ef2353edee5d172c2da936e321bbf11dec51779447534465df9c275eb391f1ada2c089ecb15', 7 | }); 8 | 9 | // For versions of the fwupater tool <= 0.1.1 10 | export const oldFwupdaterSignatures = Object.freeze({ 11 | GET_FIRMWARE_INFO: 'aceffd98d331df0daa5bb3308bb49a95767d77e7a1557c07a0ec544d2f41c3ec67269f01ce9a63e01f3b43e087ab8eb22b7f1d34135b6686e8ce27d4b5dc083ec8e6149df11880d32486448a71280ef3128efccbd45a84dbd7990a9420a65ee86b3822edba3554fa8e6ca11aec12d4dd99ad072285b98bfdf7b2b64f677da50feb8bddef25a36f52d7605078487d8a5d7cbdc84bfa65d510cee97b46baefea149139a9a6ed4b545346040536e33d850e6ad84c83fe605f677e2ca77439de3fa42350ce504ad9a49cf62c6751d4c2a284500d2c628cd52cd73b4c3e7ef08ae823eb8941383f9c6ff0686da532369d3b266ded8fdd33cca1a128068a4795920f25', 12 | UPLOAD_FIRMWARE_BOSSAC: 'b4fca4587e784952c53ad05bff80ed0b2880be885cb3f6bd9935864863cd36d3c9c26b30b2598727b03b1b409873fa2ce0560bf1933768d5ca45c64d5f0e5032b19146cba60d8f3aa2b19ba107be0c4fa592d86e44bc87f2330fc1c9995db0cc5ec884d8f294af5cbff3a81593849465eb0e4123e9b7ddb3be74b444bed40d539e724f9a41617f2e9c70b145c4a2688d47894319fd65d660ad7d8382fb56a455e28fa78042337d55f8b5ec4f2c16f3410d15c1551973eec2a80a1792c344f7835e2133f9279009a40054bf234f7bc194f7c18d0e6c7102d13823db1ad60d81b40e3e43f89eb26a0cc5fd9759286f11ba4b649829bda8a52d0013ee59e2df3b3b', 13 | UPLOAD_FIRMWARE_AVRDUDE: '38bb99d99e00d91166ef095477b24b7a52eda0685db819486a723ee09e84ea84422b7f6c973466e97b832c5afa54a84a6d2dfad17a38f8f91059c5f62212c2a3e8a887c63b3b679eb1c4c2e76f2f4e943d8bd59a5f21283c9a3a7a9b5584c14b8adfc881315bcc605d26bfe31c9b2dfa5979d501b2333963614337668f0303ef6ddd3ec72a39b448d38c6af501a1357c6b8527f27ca8a096b7bc0413b1cb5f504d44bd502cee59444c00fae11cc59c51ae8153e4e481fb58a3ee3a7f23447aa9aa368b8cc0d0d107b77b234a7528ab10cbb5dc09de874ef2a6bd427aa2f83f99faabd54b272c7df3922fa5439e54e9d8b2793bc5ce1c5979f3f5e5db03853f53' 14 | }); 15 | -------------------------------------------------------------------------------- /demo/v2/install_tool.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { flatMap } from 'rxjs/operators'; 3 | 4 | export class V2InstallTool extends React.Component { 5 | constructor() { 6 | super(); 7 | this.state = { 8 | name: 'avrdude', 9 | version: '6.3.0-arduino9', 10 | packager: 'arduino', 11 | url: 'http://downloads.arduino.cc/tools/avrdude-6.3.0-arduino9-i686-w64-mingw32.zip', 12 | checksum: 'SHA-256:f3c5cfa8d0b3b0caee81c5b35fb6acff89c342ef609bf4266734c6266a256d4f', 13 | signature: '7628b488c7ffd21ae1ca657245751a4043c419fbab5c256a020fb53f17eb88686439f54f18e78a80b40fc2de742f79b78ed4338c959216dc8ae8279e482d2d4117eeaf34a281ce2369d1dc4356f782c0940d82610f1c892e913b637391c39e95d4d4dfe82d8dbc5350b833186a70a62c7952917481bad798a9c8b4905df91bd914fbdfd6e98ef75c8f7fb06284278da449ce05b27741d6eda156bbdb906d519ff7d7d5042379fdfc55962b3777fb9240b368552182758c297e39c72943d75d177f2dbb584b2210301250796dbe8af11f0cf06d762fe4f912294f4cdc8aff26715354cfb33010a81342fbbc438912eb424a39fc0c52a9b2bf722051a6f3b024bd', 14 | res: '' 15 | }; 16 | 17 | this.handleChange = this.handleChange.bind(this); 18 | this.handleSubmit = this.handleSubmit.bind(this); 19 | } 20 | 21 | componentDidMount() { 22 | this.daemon = this.props.daemon; 23 | 24 | if (this.daemon.agentV2Found) { // agentV2Found not available for instance on chromebooks 25 | this.daemon.agentV2Found.subscribe(daemonV2 => { 26 | if (!daemonV2) { 27 | return; 28 | } 29 | this.daemonV2 = daemonV2; 30 | }); 31 | } 32 | } 33 | 34 | handleChange(event) { 35 | this.setState({ [event.target.name]: event.target.value }); 36 | } 37 | 38 | handleSubmit(event) { 39 | event.preventDefault(); 40 | 41 | this.daemonV2.installTool({ 42 | name: this.state.name, 43 | version: this.state.version, 44 | packager: this.state.packager, 45 | checksum: this.state.checksum, 46 | signature: this.state.signature, 47 | url: this.state.url, 48 | 49 | }) 50 | .then(res => { 51 | this.setState({ 52 | res: JSON.stringify(res, null, 2) 53 | }); 54 | }) 55 | .catch(err => { 56 | this.setState({ 57 | res: JSON.stringify(err, null, 2) 58 | }); 59 | }); 60 | } 61 | 62 | render() { 63 | return ( 64 |
65 |

Install a new tool

66 |
67 |
68 | 71 | 72 |
73 |
74 | 77 | 78 |
79 |
80 | 83 | 84 |
85 |
86 | 89 | 90 |
91 |
92 | 95 | 96 |
97 |
98 | 101 | 102 |
103 | 104 |
105 | 106 |
107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) 2 | [![npm version](https://badge.fury.io/js/arduino-create-agent-js-client.svg)](https://badge.fury.io/js/arduino-create-agent-js-client) 3 | 4 | # arduino-create-agent-js-client 5 | JS module providing discovery of the [Arduino Create Agent](https://github.com/arduino/arduino-create-agent) and communication with it 6 | 7 | ## Changelog 8 | See [CHANGELOG.md](https://github.com/arduino/arduino-create-agent-js-client/blob/HEAD/CHANGELOG.md) for more details. 9 | 10 | ## Installation 11 | 12 | ```bash 13 | npm install arduino-create-agent-js-client --save 14 | ``` 15 | 16 | ## How to use 17 | 18 | ```js 19 | import Daemon from 'arduino-create-agent-js-client'; 20 | 21 | const daemon = new Daemon('https://builder.arduino.cc/v3/boards'); 22 | 23 | daemon.agentFound.subscribe(status => { 24 | // true / false 25 | }); 26 | 27 | daemon.channelOpenStatus.subscribe(status => { 28 | // true / false 29 | }); 30 | 31 | daemon.error.subscribe(err => { 32 | // handle err 33 | }); 34 | 35 | // List available devices (serial/network) 36 | daemon.devicesList.subscribe(({serial, network}) => { 37 | const serialDevices = serial; 38 | const networkDevices = network; 39 | }); 40 | 41 | // Open serial monitor 42 | daemon.openSerialMonitor('port-name'); 43 | 44 | // Read from serial monitor (ouputs string) 45 | daemon.serialMonitorMessages.subscribe(message => { 46 | console.log(message); 47 | }); 48 | 49 | // Read from serial monitor, output object with source port name 50 | /* 51 | { 52 | "P": "dev/ttyACM0", 53 | "D":"output text here\r\n" 54 | } 55 | */ 56 | daemon.serialMonitorMessagesWithPort.subscribe(messageObj => { 57 | console.log(messageObj); 58 | }); 59 | 60 | // Write to serial monitor 61 | daemon.writeSerial('port-name', 'message'); 62 | 63 | // Close serial monitor 64 | daemon.closeSerialMonitor('port-name'); 65 | 66 | // Upload sketch on serial target (desktop agent and chrome app) 67 | daemon.uploadSerial(target, sketchName, compilationResult, verbose); 68 | 69 | // Upload sketch on network target (daesktop agent only) 70 | daemon.uploadNetwork(target, sketchName, compilationResult); 71 | 72 | // Upload progress 73 | daemon.uploading.subscribe(upload => { 74 | console.log(status); 75 | }); 76 | 77 | // Download tool 78 | daemon.downloadTool('toolname', 'toolversion' 'packageName', 'replacement'); 79 | 80 | // Download status 81 | daemon.downloading.subscribe(download => { 82 | console.log(download); 83 | }); 84 | 85 | ``` 86 | 87 | ## Version 2 88 | 89 | Version 2 of the arduino-create-agent aims to provide a cleaner api based on promises. 90 | It will remain confined to a v2 property on the daemon object until it will be stable. 91 | At the moment it only supports tool management. 92 | 93 | ```js 94 | daemon.agentV2Found.subscribe(daemonV2 => { 95 | if (!daemonV2) { 96 | // Your Agent doesn't support v2 97 | } 98 | // Your Agent supports v2 99 | }); 100 | 101 | daemon.v2.installedTools() 102 | .then(tools => console.debug(tools)) // [{"name":"avrdude","version":"6.3.0-arduino9","packager":"arduino"}] 103 | 104 | let payload = { 105 | name: 'avrdude', 106 | version: '6.3.0-arduino9', 107 | packager: 'arduino', 108 | url: 'http://downloads.arduino.cc/tools/avrdude-6.3.0-arduino9-i686-w64-mingw32.zip', 109 | checksum: 'SHA-256:f3c5cfa8d0b3b0caee81c5b35fb6acff89c342ef609bf4266734c6266a256d4f', 110 | signature: '7628b488c7ffd21ae1ca657245751a4043c419fbab5c256a020fb53f17eb88686439f54f18e78a80b40fc2de742f79b78ed4338c959216dc8ae8279e482d2d4117eeaf34a281ce2369d1dc4356f782c0940d82610f1c892e913b637391c39e95d4d4dfe82d8dbc5350b833186a70a62c7952917481bad798a9c8b4905df91bd914fbdfd6e98ef75c8f7fb06284278da449ce05b27741d6eda156bbdb906d519ff7d7d5042379fdfc55962b3777fb9240b368552182758c297e39c72943d75d177f2dbb584b2210301250796dbe8af11f0cf06d762fe4f912294f4cdc8aff26715354cfb33010a81342fbbc438912eb424a39fc0c52a9b2bf722051a6f3b024bd' 111 | } 112 | daemon.v2.installTool(payload) // Will install the tool in the system 113 | ``` 114 | 115 | ## Development and test features 116 | Just run `npm run dev` and open your browser on http://localhost:8000 117 | 118 | ## Agent communication 119 | 120 | To enable communication between your [local installation](http://localhost:8000/) and the [Arduino Create Agent](https://github.com/arduino/arduino-create-agent) 121 | add `origins = http://localhost:8000` on your agent config.ini file 122 | (if you are using https, add `origins = https://localhost:8000`). 123 | 124 | - On macOs ~/Applications/ArduinoCreateAgent/ArduinoCreateAgent.app/Contents/MacOS/config.ini 125 | - On Linux ~/ArduinoCreateAgent/config.ini 126 | - On Windows C:\Users\\[your user]\AppData\Roaming\ArduinoCreateAgent 127 | -------------------------------------------------------------------------------- /src/chrome-app-daemon.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 ARDUINO SA (http://www.arduino.cc/) 3 | * This file is part of arduino-create-agent-js-client. 4 | * Copyright (c) 2018 5 | * Authors: Alberto Iannaccone, Stefania Mellai, Gabriele Destefanis 6 | * 7 | * This software is released under: 8 | * The GNU General Public License, which covers the main part of 9 | * arduino-create-agent-js-client 10 | * The terms of this license can be found at: 11 | * https://www.gnu.org/licenses/gpl-3.0.en.html 12 | * 13 | * You can be released from the requirements of the above licenses by purchasing 14 | * a commercial license. Buying such a license is mandatory if you want to modify or 15 | * otherwise use the software for commercial activities involving the Arduino 16 | * software without disclosing the source code of your own applications. To purchase 17 | * a commercial license, send an email to license@arduino.cc. 18 | * 19 | */ 20 | 21 | import { interval } from 'rxjs'; 22 | import { 23 | distinctUntilChanged, filter, startWith, takeUntil 24 | } from 'rxjs/operators'; 25 | 26 | import Daemon from './daemon'; 27 | 28 | const POLLING_INTERVAL = 2000; 29 | 30 | export default class ChromeAppDaemon extends Daemon { 31 | constructor(boardsUrl, chromeExtensionId) { 32 | super(boardsUrl); 33 | this.chromeExtensionId = chromeExtensionId; 34 | this.channel = null; 35 | this.init(); 36 | } 37 | 38 | init() { 39 | this.openChannel(() => this.channel.postMessage({ 40 | command: 'listPorts' 41 | })); 42 | this.agentFound 43 | .pipe(distinctUntilChanged()) 44 | .subscribe(agentFound => { 45 | if (!agentFound) { 46 | this.findApp(); 47 | } 48 | }); 49 | } 50 | 51 | findApp() { 52 | interval(POLLING_INTERVAL) 53 | .pipe(startWith(0)) 54 | .pipe(takeUntil(this.channelOpen.pipe(filter(status => status)))) 55 | .subscribe(() => this._appConnect()); 56 | } 57 | 58 | /** 59 | * Instantiate connection and events listeners for chrome app 60 | */ 61 | _appConnect() { 62 | if (chrome.runtime) { 63 | this.channel = chrome.runtime.connect(this.chromeExtensionId); 64 | this.channel.onMessage.addListener(message => { 65 | if (message.version) { 66 | this.agentInfo = message; 67 | this.agentFound.next(true); 68 | this.channelOpen.next(true); 69 | } 70 | else { 71 | this.appMessages.next(message); 72 | } 73 | }); 74 | this.channel.onDisconnect.addListener(() => { 75 | this.channelOpen.next(false); 76 | this.agentFound.next(false); 77 | }); 78 | } 79 | } 80 | 81 | handleAppMessage(message) { 82 | if (message.ports) { 83 | this.handleListMessage(message); 84 | } 85 | 86 | if (message.supportedBoards) { 87 | this.supportedBoards.next(message.supportedBoards); 88 | } 89 | 90 | if (message.serialData) { 91 | this.serialMonitorMessages.next(message.serialData); 92 | } 93 | 94 | if (message.uploadStatus) { 95 | this.handleUploadMessage(message); 96 | } 97 | 98 | if (message.err) { 99 | this.uploading.next({ status: this.UPLOAD_ERROR, err: message.Err }); 100 | } 101 | } 102 | 103 | handleListMessage(message) { 104 | const lastDevices = this.devicesList.getValue(); 105 | if (!Daemon.devicesListAreEquals(lastDevices.serial, message.ports)) { 106 | this.devicesList.next({ 107 | serial: message.ports.map(port => ({ 108 | Name: port.name, 109 | SerialNumber: port.serialNumber, 110 | IsOpen: port.isOpen, 111 | VendorID: port.vendorId, 112 | ProductID: port.productId 113 | })), 114 | network: [] 115 | }); 116 | } 117 | } 118 | 119 | /** 120 | * Send 'close' command to all the available serial ports 121 | */ 122 | closeAllPorts() { 123 | const devices = this.devicesList.getValue().serial; 124 | devices.forEach(device => { 125 | this.channel.postMessage({ 126 | command: 'closePort', 127 | data: { 128 | name: device.Name 129 | } 130 | }); 131 | }); 132 | } 133 | 134 | /** 135 | * Send 'message' to serial port 136 | * @param {string} port the port name 137 | * @param {string} message the text to be sent to serial 138 | */ 139 | writeSerial(port, message) { 140 | this.channel.postMessage({ 141 | command: 'writePort', 142 | data: { 143 | name: port, 144 | data: message 145 | } 146 | }); 147 | } 148 | 149 | /** 150 | * Request serial port open 151 | * @param {string} port the port name 152 | */ 153 | openSerialMonitor(port, baudrate) { 154 | if (this.serialMonitorOpened.getValue()) { 155 | return; 156 | } 157 | const serialPort = this.devicesList.getValue().serial.find(p => p.Name === port); 158 | if (!serialPort) { 159 | return this.serialMonitorError.next(`Can't find port ${port}`); 160 | } 161 | this.appMessages 162 | .pipe(takeUntil(this.serialMonitorOpened.pipe(filter(open => open)))) 163 | .subscribe(message => { 164 | if (message.portOpenStatus === 'success') { 165 | this.serialMonitorOpened.next(true); 166 | } 167 | if (message.portOpenStatus === 'error') { 168 | this.serialMonitorError.next(`Failed to open serial ${port}`); 169 | } 170 | }); 171 | this.channel.postMessage({ 172 | command: 'openPort', 173 | data: { 174 | name: port, 175 | baudrate 176 | } 177 | }); 178 | } 179 | 180 | /** 181 | * Request serial port close 182 | * @param {string} port the port name 183 | */ 184 | closeSerialMonitor(port) { 185 | if (!this.serialMonitorOpened.getValue()) { 186 | return; 187 | } 188 | const serialPort = this.devicesList.getValue().serial.find(p => p.Name === port); 189 | if (!serialPort) { 190 | return this.serialMonitorError.next(`Can't find port ${port}`); 191 | } 192 | this.appMessages 193 | .pipe(takeUntil(this.serialMonitorOpened.pipe(filter(open => !open)))) 194 | .subscribe(message => { 195 | if (message.portCloseStatus === 'success') { 196 | this.serialMonitorOpened.next(false); 197 | } 198 | if (message.portCloseStatus === 'error') { 199 | this.serialMonitorError.next(`Failed to close serial ${port}`); 200 | } 201 | }); 202 | this.channel.postMessage({ 203 | command: 'closePort', 204 | data: { 205 | name: port 206 | } 207 | }); 208 | } 209 | 210 | handleUploadMessage(message) { 211 | if (this.uploading.getValue().status !== this.UPLOAD_IN_PROGRESS) { 212 | return; 213 | } 214 | switch (message.uploadStatus) { 215 | case 'message': 216 | this.uploading.next({ status: this.UPLOAD_IN_PROGRESS, msg: message.message }); 217 | break; 218 | case 'error': 219 | this.uploading.next({ status: this.UPLOAD_ERROR, err: message.message }); 220 | break; 221 | case 'success': 222 | this.uploading.next({ status: this.UPLOAD_DONE, msg: message.message }); 223 | break; 224 | default: 225 | this.uploading.next({ status: this.UPLOAD_IN_PROGRESS }); 226 | } 227 | } 228 | 229 | /** 230 | * Perform an upload via http on the daemon 231 | * @param {Object} target = { 232 | * board: "name of the board", 233 | * port: "port of the board", 234 | * commandline: "commandline to execute", 235 | * data: "compiled sketch" 236 | * } 237 | */ 238 | _upload(uploadPayload, uploadCommandInfo) { 239 | const { 240 | board, port, commandline, data, filename 241 | } = uploadPayload; 242 | const extrafiles = uploadCommandInfo && uploadCommandInfo.files && Array.isArray(uploadCommandInfo.files) ? uploadCommandInfo.files : []; 243 | try { 244 | window.oauth.token().then(token => { 245 | this.channel.postMessage({ 246 | command: 'upload', 247 | data: { 248 | board, 249 | port, 250 | commandline, 251 | data, 252 | token: token.token, 253 | filename, 254 | extrafiles 255 | } 256 | }); 257 | }); 258 | } 259 | catch (err) { 260 | this.uploading.next({ status: this.UPLOAD_ERROR, err: 'you need to be logged in on a Create site to upload by Chrome App' }); 261 | } 262 | } 263 | 264 | downloadTool() { 265 | // no need to download tool on chromeOS 266 | this.downloading.next({ status: this.DOWNLOAD_DONE }); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/firmware-updater.js: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs'; 2 | import semverCompare from 'semver-compare'; 3 | import { takeUntil, filter, first } from 'rxjs/operators'; 4 | import { fwupdaterSignatures, oldFwupdaterSignatures } from './signatures'; 5 | 6 | /* The status of the Firmware Updater Tool */ 7 | const FWUToolStatusEnum = Object.freeze({ 8 | NOPE: 'NOPE', 9 | OK: 'OK', 10 | CHECKING: 'CHECKING', 11 | ERROR: 'ERROR DOWNLOADING TOOL' 12 | }); 13 | 14 | /* The signatures needed to run the commands to use the Firmware Updater Tool */ 15 | let signatures = fwupdaterSignatures; 16 | 17 | let updaterBinaryName = 'FirmwareUploader'; 18 | 19 | function programmerFor(boardId) { 20 | if (boardId === 'uno2018') return ['{runtime.tools.avrdude}/bin/avrdude', signatures.UPLOAD_FIRMWARE_AVRDUDE]; 21 | if (boardId === 'nanorp2040connect') return [`{runtime.tools.rp2040tools.path}/rp2040load`, signatures.UPLOAD_FIRMWARE_RP2040]; 22 | 23 | return [`{runtime.tools.bossac}/bossac`, signatures.UPLOAD_FIRMWARE_BOSSAC]; 24 | } 25 | 26 | export default class FirmwareUpdater { 27 | constructor(Daemon) { 28 | this.updateStatusEnum = Object.freeze({ 29 | NOPE: 'NOPE', 30 | STARTED: 'STARTED', 31 | GETTING_INFO: 'GETTING_INFO', 32 | GOT_INFO: 'GOT_INFO', 33 | UPLOADING: 'UPLOADING', 34 | DONE: 'DONE', 35 | ERROR: 'ERROR' 36 | }); 37 | 38 | this.Daemon = Daemon; 39 | this.FWUToolStatus = FWUToolStatusEnum.NOPE; 40 | this.Daemon.downloadingDone.subscribe(() => { 41 | this.FWUToolStatus = FWUToolStatusEnum.OK; 42 | }); 43 | 44 | this.updating = new BehaviorSubject({ status: this.updateStatusEnum.NOPE }); 45 | 46 | this.updatingDone = this.updating.pipe(filter(update => update.status === this.updateStatusEnum.DONE)) 47 | .pipe(first()) 48 | .pipe(takeUntil(this.updating.pipe(filter(update => update.status === this.updateStatusEnum.ERROR)))); 49 | 50 | this.updatingError = this.updating.pipe(filter(update => update.status === this.updateStatusEnum.ERROR)) 51 | .pipe(first()) 52 | .pipe(takeUntil(this.updatingDone)); 53 | 54 | this.gotFWInfo = this.updating.pipe(filter(update => update.status === this.updateStatusEnum.GOT_INFO)) 55 | .pipe(first()) 56 | .pipe(takeUntil(this.updatingDone)) 57 | .pipe(takeUntil(this.updatingError)); 58 | } 59 | 60 | setToolVersion(version) { 61 | this.toolVersion = version; 62 | if (semverCompare(version, '0.1.2') < 0) { 63 | signatures = oldFwupdaterSignatures; 64 | updaterBinaryName = 'updater'; 65 | } 66 | } 67 | 68 | getFirmwareInfo(boardId, port, firmwareVersion) { 69 | this.firmwareVersionData = null; 70 | this.loaderPath = null; 71 | this.updating.next({ status: this.updateStatusEnum.GETTING_INFO }); 72 | let versionsList = []; 73 | let firmwareInfoMessagesSubscription; 74 | 75 | const handleFirmwareInfoMessage = message => { 76 | let versions; 77 | switch (message.ProgrammerStatus) { 78 | case 'Starting': 79 | break; 80 | case 'Busy': 81 | if (message.Msg.indexOf('Flashing with command:') >= 0) { 82 | return; 83 | } 84 | versions = JSON.parse(message.Msg); 85 | Object.keys(versions).forEach(v => { 86 | if (versions[v][0].IsLoader) { 87 | this.loaderPath = versions[v][0].Path; 88 | } 89 | else { 90 | versionsList = [...versionsList, ...versions[v]]; 91 | } 92 | }); 93 | this.firmwareVersionData = versionsList.find(version => version.Name.split(' ').splice(-1)[0].trim() === firmwareVersion); 94 | if (!this.firmwareVersionData) { 95 | this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't get firmware info: couldn't find version '${firmwareVersion}' for board '${boardId}'` }); 96 | } 97 | else { 98 | firmwareInfoMessagesSubscription.unsubscribe(); 99 | this.updating.next({ status: this.updateStatusEnum.GOT_INFO }); 100 | } 101 | break; 102 | case 'Error': 103 | this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Couldn't get firmware info: ${message.Msg}` }); 104 | firmwareInfoMessagesSubscription.unsubscribe(); 105 | break; 106 | default: 107 | break; 108 | } 109 | }; 110 | 111 | if (this.FWUToolStatus !== FWUToolStatusEnum.OK) { 112 | this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't get firmware info: couldn't find firmware updater tool` }); 113 | return; 114 | } 115 | 116 | firmwareInfoMessagesSubscription = this.Daemon.appMessages.subscribe(message => { 117 | if (message.ProgrammerStatus) { 118 | handleFirmwareInfoMessage(message); 119 | } 120 | }); 121 | const data = { 122 | board: boardId, 123 | port, 124 | commandline: `"{runtime.tools.fwupdater.path}/${updaterBinaryName}" -get_available_for {network.password}`, 125 | signature: signatures.GET_FIRMWARE_INFO, 126 | extra: { 127 | auth: { 128 | password: boardId 129 | } 130 | }, 131 | filename: 'ListFirmwareVersionsInfo.bin' 132 | }; 133 | 134 | return fetch(`${this.Daemon.pluginURL}/upload`, { 135 | method: 'POST', 136 | headers: { 137 | 'Content-Type': 'text/plain; charset=utf-8' 138 | }, 139 | body: JSON.stringify(data) 140 | }).then(response => { 141 | if (!response.ok) { 142 | this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Error fetching ${this.Daemon.pluginURL}/upload` }); 143 | 144 | } 145 | }).catch(() => { 146 | this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Coudln't list firmware versions info.` }); 147 | 148 | }); 149 | } 150 | 151 | updateFirmware(boardId, port, firmwareVersion) { 152 | this.updating.next({ status: this.updateStatusEnum.STARTED }); 153 | this.Daemon.closeSerialMonitor(port); 154 | this.Daemon.serialMonitorOpened.pipe(filter(open => !open)).pipe(first()).subscribe(() => { 155 | if (!port) { 156 | this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't update Firmware: no port selected.` }); 157 | return; 158 | } 159 | this.gotFWInfo.subscribe(() => { 160 | if (!this.firmwareVersionData) { 161 | this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't update Firmware: couldn't find version '${firmwareVersion}' for board '${boardId}'` }); 162 | return; 163 | } 164 | 165 | let updateFirmwareMessagesSubscription; 166 | 167 | const handleFirmwareUpdateMessage = message => { 168 | switch (message.ProgrammerStatus) { 169 | case 'Busy': 170 | if (message.Msg.indexOf('Operation completed: success! :-)') >= 0) { 171 | this.updating.next({ status: this.updateStatusEnum.DONE }); 172 | updateFirmwareMessagesSubscription.unsubscribe(); 173 | } 174 | break; 175 | case 'Error': 176 | this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't update Firmware: ${message.Msg}` }); 177 | updateFirmwareMessagesSubscription.unsubscribe(); 178 | break; 179 | default: 180 | break; 181 | } 182 | }; 183 | 184 | updateFirmwareMessagesSubscription = this.Daemon.appMessages.subscribe(message => { 185 | if (message.ProgrammerStatus) { 186 | handleFirmwareUpdateMessage(message); 187 | } 188 | }); 189 | 190 | const [programmer, signature] = programmerFor(boardId); 191 | 192 | if (!this.loaderPath) { 193 | this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't update Firmware: 'loaderPath' is empty or 'null'` }); 194 | return; 195 | } 196 | 197 | const data = { 198 | board: boardId, 199 | port, 200 | commandline: `"{runtime.tools.fwupdater.path}/${updaterBinaryName}" -flasher {network.password} -port {serial.port} -programmer "${programmer}"`, 201 | hex: '', 202 | extra: { 203 | auth: { 204 | password: `"${this.loaderPath}" -firmware "${this.firmwareVersionData.Path}"`, 205 | }, 206 | }, 207 | signature, 208 | filename: 'CheckFirmwareVersion.bin', 209 | }; 210 | 211 | fetch(`${this.Daemon.pluginURL}/upload`, { 212 | method: 'POST', 213 | headers: { 214 | 'Content-Type': 'text/plain; charset=utf-8' 215 | }, 216 | body: JSON.stringify(data) 217 | }).then(response => { 218 | if (!response.ok) { 219 | this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't update Firmware: error fetching ${this.Daemon.pluginURL}/upload` }); 220 | 221 | } 222 | }).catch(reason => { 223 | this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't update Firmware: ${reason}` }); 224 | 225 | }); 226 | }); 227 | this.getFirmwareInfo(boardId, port, firmwareVersion); 228 | }); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/web-serial-daemon.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 ARDUINO SA (http://www.arduino.cc/) 3 | * This file is part of arduino-create-agent-js-client. 4 | * Copyright (c) 2018 5 | * Authors: Alberto Iannaccone, Stefania Mellai, Gabriele Destefanis 6 | * 7 | * This software is released under: 8 | * The GNU General Public License, which covers the main part of 9 | * arduino-create-agent-js-client 10 | * The terms of this license can be found at: 11 | * https://www.gnu.org/licenses/gpl-3.0.en.html 12 | * 13 | * You can be released from the requirements of the above licenses by purchasing 14 | * a commercial license. Buying such a license is mandatory if you want to modify or 15 | * otherwise use the software for commercial activities involving the Arduino 16 | * software without disclosing the source code of your own applications. To purchase 17 | * a commercial license, send an email to license@arduino.cc. 18 | * 19 | */ 20 | 21 | import { 22 | distinctUntilChanged, filter, takeUntil 23 | } from 'rxjs/operators'; 24 | 25 | import Daemon from './daemon'; 26 | 27 | /** 28 | * WARNING: the WebSerialDaemon with support for the Web Serial API is still in an alpha version. 29 | * At the moment it doesn't implement all the features available in the Chrome App Deamon 30 | * Use at your own risk. 31 | * 32 | * The `channel` parameter in the constructor is the component which is 33 | * used to interact with the Web Serial API. 34 | * 35 | * It must provide a `postMessage` method, similarly to the object created with `chrome.runtime.connect` in 36 | * the `chrome-app-daemon.js` module, which is used to send messages to interact with the Web Serial API. 37 | */ 38 | export default class WebSerialDaemon extends Daemon { 39 | constructor(boardsUrl, channel) { 40 | super(boardsUrl); 41 | 42 | this.port = null; 43 | this.channelOpenStatus.next(true); 44 | this.channel = channel; // channel is injected from the client app 45 | this.connectedPorts = []; 46 | 47 | this.init(); 48 | } 49 | 50 | init() { 51 | this.agentFound 52 | .pipe(distinctUntilChanged()) 53 | .subscribe(found => { 54 | if (!found) { 55 | // Set channelOpen false for the first time 56 | if (this.channelOpen.getValue() === null) { 57 | this.channelOpen.next(false); 58 | } 59 | 60 | this.connectToChannel(); 61 | } 62 | else { 63 | this.openChannel(() => this.channel.postMessage({ 64 | command: 'listPorts' 65 | })); 66 | } 67 | }); 68 | } 69 | 70 | connectToChannel() { 71 | this.channel.onMessage(message => { 72 | if (message.version) { 73 | this.agentInfo = { 74 | version: message.version, 75 | os: 'ChromeOS' 76 | }; 77 | this.agentFound.next(true); 78 | this.channelOpen.next(true); 79 | } 80 | else { 81 | this.appMessages.next(message); 82 | } 83 | }); 84 | this.channel.onDisconnect(() => { 85 | this.channelOpen.next(false); 86 | this.agentFound.next(false); 87 | }); 88 | } 89 | 90 | handleAppMessage(message) { 91 | if (message.ports) { 92 | this.handleListMessage(message); 93 | } 94 | else if (message.supportedBoards) { 95 | this.supportedBoards.next(message.supportedBoards); 96 | } 97 | if (message.serialData) { 98 | this.serialMonitorMessages.next(message.serialData); 99 | } 100 | 101 | if (message.uploadStatus) { 102 | this.handleUploadMessage(message); 103 | } 104 | 105 | if (message.err) { 106 | this.uploading.next({ status: this.UPLOAD_ERROR, err: message.Err }); 107 | } 108 | } 109 | 110 | handleUploadMessage(message) { 111 | if (this.uploading.getValue().status !== this.UPLOAD_IN_PROGRESS) { 112 | return; 113 | } 114 | switch (message.uploadStatus) { 115 | case 'message': 116 | this.uploading.next({ 117 | status: this.UPLOAD_IN_PROGRESS, 118 | msg: message.message, 119 | operation: message.operation, 120 | port: message.port 121 | }); 122 | break; 123 | case 'error': 124 | this.uploading.next({ status: this.UPLOAD_ERROR, err: message.message }); 125 | break; 126 | case 'success': 127 | this.uploading.next( 128 | { 129 | status: this.UPLOAD_DONE, 130 | msg: message.message, 131 | operation: message.operation, 132 | port: message.port 133 | } 134 | ); 135 | break; 136 | 137 | default: 138 | this.uploading.next({ status: this.UPLOAD_IN_PROGRESS }); 139 | } 140 | } 141 | 142 | handleListMessage(message) { 143 | const lastDevices = this.devicesList.getValue(); 144 | if (!Daemon.devicesListAreEquals(lastDevices.serial, message.ports)) { 145 | this.devicesList.next({ 146 | serial: message.ports 147 | .map(port => ({ 148 | Name: port.name, 149 | SerialNumber: port.serialNumber, 150 | IsOpen: port.isOpen, 151 | VendorID: port.vendorId, 152 | ProductID: port.productId 153 | })), 154 | network: [] 155 | }); 156 | } 157 | } 158 | 159 | /** 160 | * Send 'close' command to all the available serial ports 161 | */ 162 | closeAllPorts() { 163 | const devices = this.devicesList.getValue().serial; 164 | if (Array.isArray(devices)) { 165 | devices.forEach(device => { 166 | this.channel.postMessage({ 167 | command: 'closePort', 168 | data: { 169 | name: device.Name 170 | } 171 | }); 172 | }); 173 | } 174 | } 175 | 176 | /** 177 | * Send the 'writePort' message to the serial port 178 | * @param {string} port the port name 179 | * @param {string} message the text to be sent to serial 180 | */ 181 | writeSerial(port, message) { 182 | this.channel.postMessage({ 183 | command: 'writePort', 184 | data: { 185 | name: port, 186 | data: message 187 | } 188 | }); 189 | } 190 | 191 | /** 192 | * Request serial port open 193 | * @param {string} port the port name 194 | */ 195 | openSerialMonitor(port, baudrate) { 196 | if (this.serialMonitorOpened.getValue()) { 197 | return; 198 | } 199 | const serialPort = this.devicesList.getValue().serial.find(p => p.Name === port); 200 | if (!serialPort) { 201 | return this.serialMonitorError.next(`Can't find port ${port}`); 202 | } 203 | this.appMessages 204 | .pipe(takeUntil(this.serialMonitorOpened.pipe(filter(open => open)))) 205 | .subscribe(message => { 206 | if (message.portOpenStatus === 'success') { 207 | this.serialMonitorOpened.next(true); 208 | } 209 | if (message.portOpenStatus === 'error') { 210 | this.serialMonitorError.next(`Failed to open serial ${port}`); 211 | } 212 | }); 213 | this.channel.postMessage({ 214 | command: 'openPort', 215 | data: { 216 | name: port, 217 | baudrate 218 | } 219 | }); 220 | } 221 | 222 | closeSerialMonitor(port) { 223 | if (!this.serialMonitorOpened.getValue()) { 224 | return; 225 | } 226 | const serialPort = this.devicesList.getValue().serial.find(p => p.Name === port); 227 | if (!serialPort) { 228 | return this.serialMonitorError.next(`Can't find port ${port}`); 229 | } 230 | this.appMessages 231 | .pipe(takeUntil(this.serialMonitorOpened.pipe(filter(open => !open)))) 232 | .subscribe(message => { 233 | if (message.portCloseStatus === 'success') { 234 | this.serialMonitorOpened.next(false); 235 | } 236 | if (message.portCloseStatus === 'error') { 237 | this.serialMonitorError.next(`Failed to close serial ${port}`); 238 | } 239 | }); 240 | this.channel.postMessage({ 241 | command: 'closePort', 242 | data: { 243 | name: port 244 | } 245 | }); 246 | } 247 | 248 | connectToSerialDevice({ from, dialogCustomization }) { 249 | this.channel.postMessage({ 250 | command: 'connectToSerial', 251 | data: { 252 | from, 253 | dialogCustomization 254 | } 255 | }); 256 | } 257 | 258 | /** 259 | * @param {object} uploadPayload 260 | * TODO: document param's shape 261 | */ 262 | _upload(uploadPayload, uploadCommandInfo) { 263 | const { 264 | board, port, commandline, data, pid, vid, filename, dialogCustomizations 265 | } = uploadPayload; 266 | 267 | const extrafiles = uploadCommandInfo && uploadCommandInfo.files && Array.isArray(uploadCommandInfo.files) ? uploadCommandInfo.files : []; 268 | 269 | try { 270 | window.oauth.getAccessToken().then(token => { 271 | this.channel.postMessage({ 272 | command: 'upload', 273 | data: { 274 | board, 275 | port, 276 | commandline, 277 | data, 278 | token: token.token, 279 | extrafiles, 280 | pid, 281 | vid, 282 | filename, 283 | dialogCustomizations 284 | } 285 | }); 286 | }); 287 | } 288 | catch (err) { 289 | this.uploading.next({ status: this.UPLOAD_ERROR, err: 'you need to be logged in on a Create site to upload by Chrome App' }); 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/daemon.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 ARDUINO SA (http://www.arduino.cc/) 3 | * This file is part of arduino-create-agent-js-client. 4 | * Copyright (c) 2018 5 | * Authors: Alberto Iannaccone, Stefania Mellai, Gabriele Destefanis 6 | * 7 | * This software is released under: 8 | * The GNU General Public License, which covers the main part of 9 | * arduino-create-agent-js-client 10 | * The terms of this license can be found at: 11 | * https://www.gnu.org/licenses/gpl-3.0.en.html 12 | * 13 | * You can be released from the requirements of the above licenses by purchasing 14 | * a commercial license. Buying such a license is mandatory if you want to modify or 15 | * otherwise use the software for commercial activities involving the Arduino 16 | * software without disclosing the source code of your own applications. To purchase 17 | * a commercial license, send an email to license@arduino.cc. 18 | * 19 | */ 20 | 21 | import { 22 | Subject, BehaviorSubject, interval, timer 23 | } from 'rxjs'; 24 | import { 25 | takeUntil, filter, startWith, first, distinctUntilChanged 26 | } from 'rxjs/operators'; 27 | 28 | const POLLING_INTERVAL = 1500; 29 | 30 | export default class Daemon { 31 | constructor(boardsUrl = 'https://builder.arduino.cc/v3/boards') { 32 | this.BOARDS_URL = boardsUrl; 33 | this.UPLOAD_NOPE = 'UPLOAD_NOPE'; 34 | this.UPLOAD_DONE = 'UPLOAD_DONE'; 35 | this.UPLOAD_ERROR = 'UPLOAD_ERROR'; 36 | this.UPLOAD_IN_PROGRESS = 'UPLOAD_IN_PROGRESS'; 37 | 38 | this.DOWNLOAD_DONE = 'DOWNLOAD_DONE'; 39 | this.DOWNLOAD_NOPE = 'DOWNLOAD_NOPE'; 40 | this.DOWNLOAD_ERROR = 'DOWNLOAD_ERROR'; 41 | this.DOWNLOAD_IN_PROGRESS = 'DOWNLOAD_IN_PROGRESS'; 42 | 43 | this.agentInfo = {}; 44 | this.agentFound = new BehaviorSubject(null); 45 | this.channelOpen = new BehaviorSubject(null); 46 | this.channelOpenStatus = this.channelOpen.pipe(distinctUntilChanged()); 47 | this.error = new BehaviorSubject(null).pipe(distinctUntilChanged()); 48 | this.serialMonitorError = new BehaviorSubject(null); 49 | 50 | this.appMessages = new Subject(); 51 | this.serialMonitorOpened = new BehaviorSubject(false); 52 | this.serialMonitorMessages = new Subject(); 53 | this.serialMonitorMessagesWithPort = new Subject(); 54 | this.uploading = new BehaviorSubject({ status: this.UPLOAD_NOPE }); 55 | this.uploadingDone = this.uploading.pipe(filter(upload => upload.status === this.UPLOAD_DONE)) 56 | .pipe(first()) 57 | .pipe(takeUntil(this.uploading.pipe(filter(upload => upload.status === this.UPLOAD_ERROR)))); 58 | this.uploadingError = this.uploading.pipe(filter(upload => upload.status === this.UPLOAD_ERROR)) 59 | .pipe(first()) 60 | .pipe(takeUntil(this.uploadingDone)); 61 | this.uploadInProgress = this.uploading.pipe(filter(upload => upload.status === this.UPLOAD_IN_PROGRESS)); 62 | this.devicesList = new BehaviorSubject({ 63 | serial: [], 64 | network: [] 65 | }); 66 | this.supportedBoards = new BehaviorSubject([]); 67 | this.appMessages 68 | .subscribe(message => this.handleAppMessage(message)); 69 | 70 | // Close all serial ports on startup 71 | this.devicesList 72 | .pipe(filter(devices => devices.serial && devices.serial.length > 0)) 73 | .pipe(first()) 74 | .subscribe(() => this.closeAllPorts()); 75 | 76 | this.downloading = new BehaviorSubject({ status: this.DOWNLOAD_NOPE }); 77 | this.downloadingDone = this.downloading.pipe(filter(download => download.status === this.DOWNLOAD_DONE)) 78 | .pipe(first()) 79 | .pipe(takeUntil(this.downloading.pipe(filter(download => download.status === this.DOWNLOAD_ERROR)))); 80 | this.downloadingError = this.downloading.pipe(filter(download => download.status === this.DOWNLOAD_ERROR)) 81 | .pipe(first()) 82 | .pipe(takeUntil(this.downloadingDone)); 83 | 84 | this.boardPortAfterUpload = new Subject().pipe(first()); 85 | this.uploadingPort = null; 86 | } 87 | 88 | notifyUploadError(err) { 89 | this.uploading.next({ status: this.UPLOAD_ERROR, err }); 90 | } 91 | 92 | openChannel(cb) { 93 | this.channelOpen 94 | .subscribe(open => { 95 | if (open) { 96 | interval(POLLING_INTERVAL) 97 | .pipe(startWith(0)) 98 | .pipe(takeUntil(this.channelOpen.pipe(filter(status => !status)))) 99 | .subscribe(cb); 100 | } 101 | else { 102 | this.devicesList.next({ 103 | serial: [], 104 | network: [] 105 | }); 106 | this.agentFound.next(false); 107 | } 108 | }); 109 | } 110 | 111 | /** 112 | * Upload a sketch to serial target 113 | * Fetch commandline from boards API for serial upload 114 | * @param {Object} target 115 | * @param {string} sketchName 116 | * @param {Object} compilationResult 117 | * @param {boolean} verbose 118 | * @param {any[]} dialogCustomizations Optional - Used in Web Serial API to customize the permission dialogs. 119 | * It's an array because the Web Serial API library can use more than one dialog, e.g. one to 120 | * ask permission and one to give instruction to save an UF2 file. 121 | * It's called 'customizations' because the library already provides a basic non-styled dialog. 122 | */ 123 | uploadSerial(target, sketchName, compilationResult, verbose = true, dialogCustomizations) { 124 | this.uploadingPort = target.port; 125 | this.uploading.next({ status: this.UPLOAD_IN_PROGRESS, msg: 'Upload started' }); 126 | this.serialDevicesBeforeUpload = this.devicesList.getValue().serial; 127 | 128 | this.closeSerialMonitor(target.port); 129 | 130 | // Fetch command line for the board 131 | fetch(`${this.BOARDS_URL}/${target.board}/compute`, { 132 | method: 'POST', 133 | body: JSON.stringify({ action: 'upload', verbose, os: this.agentInfo.os }) 134 | }) 135 | .then(result => result.json()) 136 | .then(uploadCommandInfo => { 137 | let ext = Daemon._extractExtensionFromCommandline(uploadCommandInfo.commandline); 138 | const data = compilationResult[ext] || compilationResult.bin; 139 | if (!ext || !data) { 140 | console.log('we received a faulty ext property, defaulting to .bin'); 141 | ext = 'bin'; 142 | } 143 | 144 | const uploadPayload = { 145 | ...target, 146 | commandline: uploadCommandInfo.commandline, 147 | filename: `${sketchName}.${ext}`, 148 | hex: data, // For desktop agent 149 | data, // For chromeOS plugin, consider to align this 150 | dialogCustomizations // used only in Web Serial API uploader 151 | }; 152 | 153 | this.uploadingDone.subscribe(() => { 154 | this.waitingForPortToComeUp = timer(1000).subscribe(() => { 155 | const currentSerialDevices = this.devicesList.getValue().serial; 156 | let boardFound = currentSerialDevices.find(device => device.Name === this.uploadingPort); 157 | if (!boardFound) { 158 | boardFound = currentSerialDevices.find(d => this.serialDevicesBeforeUpload.every(e => e.Name !== d.Name)); 159 | if (boardFound) { 160 | this.uploadingPort = boardFound.Name; 161 | this.boardPortAfterUpload.next({ 162 | hasChanged: true, 163 | newPort: this.uploadingPort 164 | }); 165 | } 166 | } 167 | 168 | if (boardFound) { 169 | this.waitingForPortToComeUp.unsubscribe(); 170 | this.uploadingPort = null; 171 | this.serialDevicesBeforeUpload = null; 172 | this.boardPortAfterUpload.next({ 173 | hasChanged: false 174 | }); 175 | } 176 | }); 177 | }); 178 | const files = [...(uploadCommandInfo.files || []), ...(compilationResult.files || [])]; 179 | this._upload(uploadPayload, { ...uploadCommandInfo, files }); 180 | }); 181 | } 182 | 183 | /** 184 | * Compares 2 devices list checking they contains the same ports in the same order 185 | * @param {Array} a the first list 186 | * @param {Array} b the second list 187 | */ 188 | static devicesListAreEquals(a, b) { 189 | if (!a || !b || a.length !== b.length) { 190 | return false; 191 | } 192 | return a.every((item, index) => (b[index].Name === item.Name && b[index].IsOpen === item.IsOpen)); 193 | } 194 | 195 | /** 196 | * Interrupt upload - not supported in Chrome app 197 | */ 198 | stopUpload() { 199 | if (typeof this.stopUploadCommand === 'function') { 200 | this.stopUploadCommand(); 201 | } 202 | else { 203 | throw new Error('Stop Upload not supported on Chrome OS'); 204 | } 205 | } 206 | 207 | /** 208 | * Set the board in bootloader mode. This is needed to bo 100% sure to receive the correct vid/pid from the board. 209 | * To do that we just touch the port at 1200 bps and then close it. The sketch on the board will be erased. 210 | * @param {String} port the port name 211 | */ 212 | setBootloaderMode(port) { 213 | this.serialMonitorOpened.pipe(filter(open => open)).pipe(first()).subscribe(() => { 214 | timer(1000).subscribe(() => this.closeSerialMonitor(port)); 215 | }); 216 | this.openSerialMonitor(port, 1200); 217 | } 218 | 219 | static _extractExtensionFromCommandline(commandline) { 220 | const rx = /\{build\.project_name\}\.(\w\w\w)\b/g; 221 | const arr = rx.exec(commandline); 222 | if (arr && arr.length > 0) { 223 | return arr[1]; 224 | } 225 | return null; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /demo/serial_mirror.js: -------------------------------------------------------------------------------- 1 | export const HEX = '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CgD/////AgABAAAAAwAAAAEAAAAEAAAACwD/////AwAAAAAABAAAAAEAAAAcAAAABAAAAAAA/wAAAAAABQAAAAEAAAAcAAAABQABAAEA/wAAAAAABgAAAAEAAAAEAAAABgD//////wAAAAAABwAAAAEAAAAEAAAABwD//////wAAAAAAGAAAAAYAAAAAAAAA/wD//////wAAAAAAGQAAAAYAAAAAAAAA/wD//////wAAAAAAEgAAAAgAAAAEAAAA/wD//////wAAAAAAAwAAAAgAAAAEAAAA/wD//////wAAAAAADAAAAAIAAAAAAAAA/wD//////wAAAAAADQAAAAIAAAAAAAAA/wD//////wAAAAAADgAAAAgAAAAAAAAA/wD//////wAAAAAADwAAAAIAAAAAAAAA/wD//////wAAAAAAGwAAAAgAAAAAAAAA/wD//////wAAAAAAHAAAAAgAAAAAAAAA/wD//////wABAAAACAAAAAgAAAAAAAAA/wD//////wABAAAACQAAAAgAAAAAAAAA/wD/////CQAAAAAAAAAAAAgAAAAAAAAA/wD//////wAAAAAAAQAAAAgAAAAAAAAA/wD//////wAAAAAAAAAAAPUjAAAhJAAA8SMAABUkAAAJJAAAWSQAAD0kAAAAAAAAAAAAAAAAAACdKAAAgScAAMkmAAAAAAAAQXJkdWlubyBMTEMABAMJBBIBAALvAgFAQSNOgAABAQIDAUFyZHVpbm8gTUtSMTAwMAAAAAAAAAAAAAAAAAAAAJk3AADRPQAA2zYAAJ02AAC1NgAAUTgAAHU0AABlNAAAnTUAAIs0AABhNAAAAAAAAEMAAAAAAAAAAMIBAAAACAD/////CAsAAgICAAAJBAAAAQICAAAFJAAQAQQkAgYFJAYAAQUkAQEBBwWBAxAAEAkEAQACCgAAAAcFAgJAAAAHBYMCQAAAAAD/////AAAAAIMAAAACAAAAggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBCDwD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoRQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhAAAIN0gAABdIQAAHSUAAFE0AAC1IAAAAAAAAA=='; 2 | -------------------------------------------------------------------------------- /demo/app.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 ARDUINO SA (http://www.arduino.cc/) 3 | * This file is part of arduino-create-agent-js-client. 4 | * Copyright (c) 2018 5 | * Authors: Alberto Iannaccone, Stefania Mellai, Gabriele Destefanis 6 | * 7 | * This software is released under: 8 | * The GNU General Public License, which covers the main part of 9 | * arduino-create-agent-js-client 10 | * The terms of this license can be found at: 11 | * https://www.gnu.org/licenses/gpl-3.0.en.html 12 | * 13 | * You can be released from the requirements of the above licenses by purchasing 14 | * a commercial license. Buying such a license is mandatory if you want to modify or 15 | * otherwise use the software for commercial activities involving the Arduino 16 | * software without disclosing the source code of your own applications. To purchase 17 | * a commercial license, send an email to license@arduino.cc. 18 | * 19 | */ 20 | 21 | import React from 'react'; 22 | import Daemon from '../src'; 23 | import FirmwareUpdater from '../src/firmware-updater'; 24 | 25 | import { HEX } from './serial_mirror'; 26 | import V2 from './v2/v2.jsx'; 27 | 28 | const chromeExtensionID = 'hfejhkbipnickajaidoppbadcomekkde'; 29 | 30 | const isChromeOs = () => window.navigator.userAgent.indexOf(' CrOS ') !== -1; 31 | 32 | const scrollToBottom = (target) => { 33 | if (target) { 34 | target.scrollTop = target.scrollHeight; // eslint-disable-line no-param-reassign 35 | } 36 | }; 37 | 38 | const daemon = new Daemon('https://builder.arduino.cc/v3/boards', chromeExtensionID); 39 | const firmwareUpdater = new FirmwareUpdater(daemon); 40 | 41 | const handleBootloaderMode = (e, port) => { 42 | e.preventDefault(); 43 | daemon.setBootloaderMode(port); 44 | }; 45 | 46 | const handleUpdateFirmware = (e, boardId, port, firmwareVersion) => { 47 | e.preventDefault(); 48 | if (![firmwareUpdater.updateStatusEnum.NOPE, firmwareUpdater.updateStatusEnum.DONE, firmwareUpdater.updateStatusEnum.ERROR].includes(firmwareUpdater.updating.getValue().status)) { 49 | return; 50 | } 51 | firmwareUpdater.updateFirmware(boardId, port, firmwareVersion); 52 | firmwareUpdater.updatingDone.subscribe(() => { 53 | console.log('Firmware updated successfully!'); 54 | }); 55 | 56 | firmwareUpdater.updatingError.subscribe(update => { 57 | console.log('Something went wrong when trying to update the firmware'); 58 | console.error(update.err); 59 | }); 60 | }; 61 | 62 | const handleDownloadTool = e => { 63 | e.preventDefault(); 64 | const toolname = document.getElementById('toolname'); 65 | const toolversion = document.getElementById('toolversion'); 66 | const packageName = document.getElementById('package'); 67 | const replacement = document.getElementById('replacement'); 68 | daemon.downloadTool(toolname.value, toolversion.value, packageName.value, replacement.value); 69 | toolname.value = ''; 70 | toolversion.value = ''; 71 | packageName.value = ''; 72 | replacement.value = ''; 73 | }; 74 | 75 | class App extends React.Component { 76 | constructor() { 77 | super(); 78 | this.state = { 79 | error: '', 80 | agentStatus: false, 81 | channelStatus: false, 82 | serialDevices: [], 83 | networkDevices: [], 84 | agentInfo: '', 85 | serialMonitorContent: '', 86 | serialPortOpen: '', 87 | uploadStatus: '', 88 | uploadError: '', 89 | downloadStatus: '', 90 | downloadError: '', 91 | serialInput: '', 92 | supportedBoards: [], 93 | uploadingPort: '' 94 | }; 95 | this.handleOpen = this.handleOpen.bind(this); 96 | this.handleClose = this.handleClose.bind(this); 97 | this.handleSend = this.handleSend.bind(this); 98 | this.handleChangeSerial = this.handleChangeSerial.bind(this); 99 | this.showError = this.showError.bind(this); 100 | this.clearError = this.clearError.bind(this); 101 | this.handleUpload = this.handleUpload.bind(this); 102 | } 103 | 104 | componentDidMount() { 105 | daemon.agentFound.subscribe(status => { 106 | this.setState({ 107 | agentStatus: status, 108 | agentInfo: JSON.stringify(daemon.agentInfo, null, 2) 109 | }); 110 | }); 111 | 112 | daemon.channelOpen.subscribe(status => { 113 | this.setState({ channelStatus: status }); 114 | }); 115 | 116 | daemon.error.subscribe(this.showError); 117 | daemon.serialMonitorError.subscribe(this.showError); 118 | daemon.uploadingError.subscribe(this.showError); 119 | daemon.downloadingError.subscribe(this.showError); 120 | 121 | daemon.devicesList.subscribe(({ serial, network }) => this.setState({ 122 | serialDevices: serial, 123 | networkDevices: network 124 | })); 125 | 126 | daemon.supportedBoards.subscribe(boards => this.setState({ 127 | supportedBoards: boards 128 | })); 129 | 130 | const serialTextarea = document.getElementById('serial-textarea'); 131 | 132 | daemon.serialMonitorMessages.subscribe(message => { 133 | this.setState({ 134 | serialMonitorContent: this.state.serialMonitorContent + message 135 | }); 136 | scrollToBottom(serialTextarea); 137 | }); 138 | 139 | daemon.serialMonitorMessagesWithPort.subscribe(messageObj => { 140 | console.log(messageObj); 141 | }); 142 | 143 | daemon.uploading.subscribe(upload => { 144 | this.setState({ uploadStatus: upload.status, uploadError: upload.err }); 145 | // console.log(upload); 146 | }); 147 | 148 | if (daemon.downloading) { 149 | daemon.downloading.subscribe(download => { 150 | this.setState({ downloadStatus: download.status }); 151 | // console.log(download); 152 | }); 153 | } 154 | } 155 | 156 | requestDevicePermission = () => { 157 | if ('serial' in navigator) { 158 | navigator.serial.requestPort([{ usbVendorId: 0x2341 }]).then((port) => { 159 | daemon.devicesList.next({ 160 | serial: [port], 161 | network: [] 162 | }); 163 | }); 164 | } 165 | }; 166 | 167 | showError(err) { 168 | this.setState({ error: err }); 169 | scrollToBottom(document.body); 170 | } 171 | 172 | clearError() { 173 | this.setState({ error: '' }); 174 | } 175 | 176 | handleOpen(e, port) { 177 | this.setState({ serialMonitorContent: '' }); 178 | e.preventDefault(); 179 | daemon.openSerialMonitor(port, 9600); 180 | this.setState({ serialPortOpen: port }); 181 | } 182 | 183 | handleClose(e, port) { 184 | e.preventDefault(); 185 | daemon.closeSerialMonitor(port); 186 | this.setState({ serialPortOpen: null }); 187 | } 188 | 189 | handleChangeSerial(e) { 190 | this.setState({ serialInput: e.target.value }); 191 | } 192 | 193 | handleSend(e) { 194 | e.preventDefault(); 195 | const serialInput = document.getElementById('serial-input'); 196 | const sendData = `${this.state.serialInput}\n`; 197 | daemon.writeSerial(this.state.serialPortOpen, sendData); 198 | serialInput.focus(); 199 | this.setState({ serialInput: '' }); 200 | } 201 | 202 | handleUpload() { 203 | const target = { 204 | board: 'arduino:samd:mkr1000', 205 | port: '/dev/ttyACM1', 206 | network: false 207 | }; 208 | 209 | this.setState({ uploadingPort: target.port }); 210 | daemon.boardPortAfterUpload.subscribe(portStatus => { 211 | if (portStatus.hasChanged) { 212 | this.setState({ uploadingPort: portStatus.newPort }); 213 | } 214 | }); 215 | 216 | // Upload a compiled sketch. 217 | daemon.uploadSerial(target, 'serial_mirror', { bin: HEX }); 218 | } 219 | 220 | render() { 221 | const listSerialDevices = this.state.serialDevices.map((device, i) =>
  • 222 | {device.Name} - IsOpen: 223 | {device.IsOpen ? 'true' : 'false'} 224 | - this.handleOpen(e, device.Name)}> 225 | open 226 | - this.handleClose(e, device.Name)}> 227 | close 228 | - handleBootloaderMode(e, device.Name)}> 229 | bootloader mode 230 | - handleUpdateFirmware(e, 'mkrwifi1010', device.Name, '1.2.1')}> 231 | MKR WiFi 1010 update firmware 232 | - handleUpdateFirmware(e, 'mkr1000', device.Name, '19.4.4')}> 233 | MKR1000 update firmware 234 | 235 |
  • ); 236 | 237 | const listNetworkDevices = this.state.networkDevices.map((device, i) =>
  • 238 | {device.Name} 239 |
  • ); 240 | 241 | const supportedBoards = this.state.supportedBoards.map((board, i) =>
  • 242 | { board } 243 |
  • ); 244 | 245 | let uploadClass; 246 | if (this.state.uploadStatus === daemon.UPLOAD_DONE) { 247 | uploadClass = 'success'; 248 | } 249 | else if (this.state.uploadStatus === daemon.UPLOAD_ERROR) { 250 | uploadClass = 'error'; 251 | } 252 | else if (this.state.uploadStatus === daemon.UPLOAD_IN_PROGRESS) { 253 | uploadClass = 'in-progress'; 254 | } 255 | 256 | let downloadClass; 257 | if (this.state.downloadStatus === daemon.DOWNLOAD_DONE) { 258 | downloadClass = 'success'; 259 | } 260 | else if (this.state.downloadStatus === daemon.DOWNLOAD_ERROR) { 261 | downloadClass = 'error'; 262 | } 263 | else if (this.state.downloadStatus === daemon.DOWNLOAD_IN_PROGRESS) { 264 | downloadClass = 'in-progress'; 265 | } 266 | 267 | return ( 268 |
    269 |

    Arduino Create Plugin Client Demo

    270 | 271 |
    272 |

    Plugin info

    273 | 274 |

    275 | Agent status: 276 | { this.state.agentStatus ? 'Found' : 'Not found' } 277 | 278 |

    279 |

    280 | Channel status: 281 | { this.state.channelStatus ? 'Connected' : 'Not connected' } 282 | 283 |

    284 | 285 |
    286 |             { this.state.agentInfo }
    287 |           
    288 |
    289 | 290 |
    291 |
    292 |

    Connected Devices

    293 | { isChromeOs() && } 294 |
    295 | serial: 296 |
      297 | { listSerialDevices } 298 |
    299 | 300 | network: 301 |
      302 | { listNetworkDevices } 303 |
    304 | 305 |

    306 |
    307 | 308 | { 309 | this.state.supportedBoards.length 310 | ?
    311 |

    Supported boards

    312 | 313 |
      314 | {supportedBoards} 315 |
    316 |
    317 | : null 318 | } 319 | 320 |
    321 |

    Serial Monitor

    322 | 323 |
    324 | 325 | 326 |
    327 | 328 |