diff --git a/demo/app.jsx b/demo/app.jsx index b9f73a22..0f90bec8 100644 --- a/demo/app.jsx +++ b/demo/app.jsx @@ -20,6 +20,7 @@ import React from 'react'; import Daemon from '../src'; +import FirmwareUpdater from '../src/firmware-updater'; import { HEX } from './serial_mirror'; import V2 from './v2/v2.jsx'; @@ -33,6 +34,7 @@ const scrollToBottom = (target) => { }; const daemon = new Daemon('https://builder.arduino.cc/v3/boards', chromeExtensionID); +const firmwareUpdater = new FirmwareUpdater(daemon); const handleUpload = () => { const target = { @@ -50,6 +52,22 @@ const handleBootloaderMode = (e, port) => { daemon.setBootloaderMode(port); }; +const handleUpdateFirmware = (e, boardId, port, wifiModule) => { + e.preventDefault(); + if (![firmwareUpdater.updateStatusEnum.NOPE, firmwareUpdater.updateStatusEnum.DONE, firmwareUpdater.updateStatusEnum.ERROR].includes(firmwareUpdater.updating.getValue().status)) { + return; + } + firmwareUpdater.updateFirmware(boardId, port, wifiModule); + firmwareUpdater.updatingDone.subscribe(() => { + console.log('Firmware updated successfully!'); + }); + + firmwareUpdater.updatingError.subscribe(update => { + console.log('Something went wrong when trying to update the firmware'); + console.error(update.err); + }); +}; + const handleDownloadTool = e => { e.preventDefault(); const toolname = document.getElementById('toolname'); @@ -179,6 +197,10 @@ class App extends React.Component { close - handleBootloaderMode(e, device.Name)}> bootloader mode + - handleUpdateFirmware(e, 'mkrwifi1010', device.Name, 'wifiNina')}> + Wifi NINA update firmware + - handleUpdateFirmware(e, 'mkr1000', device.Name, 'wifi101')}> + Wifi 101 update firmware ); diff --git a/package-lock.json b/package-lock.json index 2159039f..edb0dd9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "arduino-create-agent-js-client", - "version": "2.2.3", + "version": "2.2.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fb0ad6b4..dc4eeda5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "arduino-create-agent-js-client", - "version": "2.2.3", + "version": "2.2.4", "description": "JS module providing discovery of the Arduino Create Plugin and communication with it", "main": "lib/index.js", "module": "es/index.js", diff --git a/src/firmware-updater.js b/src/firmware-updater.js new file mode 100644 index 00000000..be5bd240 --- /dev/null +++ b/src/firmware-updater.js @@ -0,0 +1,248 @@ +import {BehaviorSubject} from 'rxjs'; + +import { + takeUntil, + filter, + first +} from 'rxjs/operators'; + +const versionsMap = { + wifi101: { + latestVersion: 'WINC1500 19.5.4', + loaderPath: null, + latestVersionPath: null + }, + wifiNina: { + latestVersion: 'NINA 1.2.2', + loaderPath: null, + latestVersionPath: null + } +}; + +/* The status of the Firmware Updater Tool */ +const FWUToolStatusEnum = Object.freeze({ + NOPE: 'NOPE', + OK: 'OK', + CHECKING: 'CHECKING', + ERROR: 'ERROR DOWNLOADING TOOL' +}); + +/* The signatures needed to run the commands to use the Firmware Updater Tool */ +const signaturesEnum = Object.freeze({ + GET_FIRMWARE_INFO: 'aceffd98d331df0daa5bb3308bb49a95767d77e7a1557c07a0ec544d2f41c3ec67269f01ce9a63e01f3b43e087ab8eb22b7f1d34135b6686e8ce27d4b5dc083ec8e6149df11880d32486448a71280ef3128efccbd45a84dbd7990a9420a65ee86b3822edba3554fa8e6ca11aec12d4dd99ad072285b98bfdf7b2b64f677da50feb8bddef25a36f52d7605078487d8a5d7cbdc84bfa65d510cee97b46baefea149139a9a6ed4b545346040536e33d850e6ad84c83fe605f677e2ca77439de3fa42350ce504ad9a49cf62c6751d4c2a284500d2c628cd52cd73b4c3e7ef08ae823eb8941383f9c6ff0686da532369d3b266ded8fdd33cca1a128068a4795920f25', + UPLOAD_FIRMWARE_BOSSAC: 'b2137435f74601a0b1b090ca1592b14199c1c79c87cecac4bb168456b570f17b635f438f5cd80649a101ab27085394e4a0e92f41a2b1b65932789eee8c3ebf39633a503a8a53d3291944afe10ff88f6ea06b0fd7fd7de6d70b302df3a92091a2abe2691ad1aeee051e4bb69f6afa3bd41e643855769347dca018804ae97bcb1331df796fd8921a11b333b45bebe430d323ddd151a907e8fb0e875a45d093cdb4332ec3dbd72a50341b538e058b3f25d4a528bea514b96b8e0701032d64232b7141c62f2231352f4197a1011292a4c3e900c133824e148f3703ea8374873d9097578146819e62685f1a8cbc442dd435ea603136c86ecf028df39fd10a8b3499c6', + UPLOAD_FIRMWARE_AVRDUDE: '68ed3d9abdc7c77d01223d09c7ae55b08b8ff94f2a42d21a672effb4bdeeb12b10177e831805b3037d9f8d38e8eeeb8327b6c4691731a2a146cfd12398e5a12596e097160ae8d84fc488650ee57439f7fcd83f27d01e9834de555ceec7dc6951b3be5e5a3507752dce8e4ba1f6f9e6494162d537009db899882b9c4fca0868ad446b82fdeabf93bc30c5a39f8fe9c25e799842a10f4171e2896a0e19667b34258f06494663a4a102bcf9fd61d1ff8ebec18204bbd1a66de3e0b53e257ce521b41999dc82428539086ae9a4a5ea7112d87c05a2782cdace0e6576b162294f9ba47c658cc40999ba31be8a129689a703f6c4182055a600b6e41d450fad2896180d' +}); + + +export default class FirmwareUpdater { + constructor(Daemon) { + this.updateStatusEnum = Object.freeze({ + NOPE: 'NOPE', + STARTED: 'STARTED', + GETTING_INFO: 'GETTING_INFO', + GOT_INFO: 'GOT_INFO', + UPLOADING: 'UPLOADING', + DONE: 'DONE', + ERROR: 'ERROR' + }); + + this.Daemon = Daemon; + this.Daemon.downloadingDone.subscribe(() => { + this.FWUToolStatus = FWUToolStatusEnum.OK; + }); + + this.FWUToolStatus = FWUToolStatusEnum.NOPE; + this.updating = new BehaviorSubject({ status: this.updateStatusEnum.NOPE }); + + this.updatingDone = this.updating.pipe(filter(update => update.status === this.updateStatusEnum.DONE)) + .pipe(first()) + .pipe(takeUntil(this.updating.pipe(filter(update => update.status === this.updateStatusEnum.ERROR)))); + + this.updatingError = this.updating.pipe(filter(update => update.status === this.updateStatusEnum.ERROR)) + .pipe(first()) + .pipe(takeUntil(this.updatingDone)); + + this.gotFWInfo = this.updating.pipe(filter(update => update.status === this.updateStatusEnum.GOT_INFO)) + .pipe(first()) + .pipe(takeUntil(this.updatingDone)) + .pipe(takeUntil(this.updatingError)); + } + + getFirmwareInfo(boardId, port, wifiModule) { + this.updating.next({ status: this.updateStatusEnum.GETTING_INFO }); + let versionsList = []; + let firmwareInfoMessagesSubscription; + + const handleFirmwareInfoMessage = message => { + switch (message.ProgrammerStatus) { + case 'Starting': + break; + case 'Busy': + if (message.Msg.indexOf('Flashing with command:') >= 0) { + return; + } + const versions = JSON.parse(message.Msg); + Object.keys(versions).forEach(v => { + if (versions[v][0].IsLoader) { + versionsMap[wifiModule].loaderPath = versions[v][0].Path; + } + else { + versionsList = [...versionsList, ...versions[v]]; + } + }); + const latestVersion = versionsList.find(version => version.Name === versionsMap[wifiModule].latestVersion) + versionsMap[wifiModule].latestVersionPath = latestVersion.Path; + break; + case 'Error': + this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Couldn't get firmware info: ${message}` }) + firmwareInfoMessagesSubscription.unsubscribe(); + break; + case 'Done': + firmwareInfoMessagesSubscription.unsubscribe(); + this.updating.next({ status: this.updateStatusEnum.GOT_INFO, versionsList }) + break; + default: + break; + } + } + + if (this.FWUToolStatus !== FWUToolStatusEnum.OK) { + this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't get firmware info: couldn't find firmware updater tool` }); + return; + } + + firmwareInfoMessagesSubscription = this.Daemon.appMessages.subscribe(message => { + if (message.ProgrammerStatus) { + handleFirmwareInfoMessage(message); + } + }); + const data = { + board: boardId, + port, + commandline: `"{runtime.tools.fwupdater.path}/updater" -get_available_for {network.password}`, + signature: signaturesEnum.GET_FIRMWARE_INFO, + extra: { + auth: { + password: boardId + } + }, + filename: 'ListFirmwareVersionsInfo.bin' + }; + + return fetch(`${this.Daemon.pluginURL}/upload`, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + }, + body: JSON.stringify(data) + }).then(response => { + if (!response.ok) { + this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Error fetching ${this.Daemon.pluginURL}/upload` }); + return; + } + }).catch(reason => { + this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Coudln't list firmware versions info.` }); + return ; + }); + } + + /* + wifiModule can be either 'wifiNina' or 'wifi101' + */ + updateFirmware(boardId, port, wifiModule) { + this.updating.next({ status: this.updateStatusEnum.STARTED }); + if (!port) { + this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't update Firmware: no port selected.` }); + return; + } + this.gotFWInfo.subscribe(state => { + if (!versionsMap[wifiModule] && !versionsMap[wifiModule].latestVersion) { + this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't update Firmware: couldn't find module '${wifiModule}'` }); + return; + } + const latestVersion = state.versionsList.find(version => version.Name === versionsMap[wifiModule].latestVersion); + if (!latestVersion) { + this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't update Firmware: couldn't find version '${versionsMap[wifiModule].latestVersion}' for module '${versionsMap[wifiModule]}'` }); + return; + } + + let updateFirmwareMessagesSubscription; + + const handleFirmwareUpdateMessage = message => { + switch (message.ProgrammerStatus) { + case 'Error': + this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't update Firmware: ${message.Msg}`}); + updateFirmwareMessagesSubscription.unsubscribe(); + break; + case 'Done': + this.updating.next({ status: this.updateStatusEnum.DONE}); + updateFirmwareMessagesSubscription.unsubscribe(); + break; + default: + break; + } + } + + updateFirmwareMessagesSubscription = this.Daemon.appMessages.subscribe(message => { + if (message.ProgrammerStatus) { + handleFirmwareUpdateMessage(message); + } + }); + + let addresses = ''; + const rootCertificates = [{ + domain: 'arduino.cc', + port: 443 + }]; + + rootCertificates.forEach(address => { + if (address.domain && address.port) { + addresses += `-address ${address.domain}:${address.port} `; + } + }); + + const isUsingAvrdude = boardId === 'uno2018'; + const programmer = isUsingAvrdude ? '{runtime.tools.avrdude}/bin/avrdude' : '{runtime.tools.bossac}/bossac'; + + const loaderPath = versionsMap[wifiModule].loaderPath; + if (!loaderPath) { + this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't update Firmware: invalid loaderPath` }); + return; + } + + const data = { + board: boardId, + port, + commandline: `"{runtime.tools.fwupdater.path}/updater" -flasher {network.password} -firmware {upload.verbose} -port {serial.port} -restore_binary "{build.path}/{build.project_name}.bin" -programmer ${programmer}`, + hex: '', + extra: { + auth: { + password: loaderPath + }, + verbose: true, + params_verbose: `${versionsMap[wifiModule].latestVersionPath} ${addresses}` // eslint-disable-line camelcase + }, + signature: isUsingAvrdude ? signaturesEnum.UPLOAD_FIRMWARE_AVRDUDE : signaturesEnum.UPLOAD_FIRMWARE_BOSSAC, + filename: 'CheckFirmwareVersion.bin' + }; + + fetch(`${this.Daemon.pluginURL}/upload`, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + }, + body: JSON.stringify(data) + }).then(response => { + if (!response.ok) { + this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't update Firmware: error fetching ${this.Daemon.pluginURL}/upload` }); + return; + } + }).catch(reason => { + this.updating.next({ status: this.updateStatusEnum.ERROR, err: `Can't update Firmware: ${reason}` }); + return; + }); + }); + this.getFirmwareInfo(boardId, port, wifiModule); + } +} diff --git a/src/index.js b/src/index.js index b13b8452..997c6d0f 100644 --- a/src/index.js +++ b/src/index.js @@ -20,8 +20,9 @@ import SocketDaemon from './socket-daemon'; import ChromeOsDaemon from './chrome-app-daemon'; - +import FirmwareUpdater from './firmware-updater'; const Daemon = window.navigator.userAgent.indexOf(' CrOS ') !== -1 ? ChromeOsDaemon : SocketDaemon; export default Daemon; +export {FirmwareUpdater}; diff --git a/src/socket-daemon.js b/src/socket-daemon.js index f7b1b47d..45e32cad 100644 --- a/src/socket-daemon.js +++ b/src/socket-daemon.js @@ -209,6 +209,7 @@ export default class SocketDaemon extends Daemon { if (!driversRequested) { this.downloadTool('windows-drivers', 'latest', 'arduino'); this.downloadTool('bossac', '1.7.0', 'arduino'); + this.downloadTool('fwupdater', 'latest', 'arduino'); driversRequested = false; }