diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..158c006 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +v14.16.0 diff --git a/README.md b/README.md index 3dafef2..04f759c 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,15 @@ JS module providing discovery of the [Arduino Create Agent](https://github.com/a ## Changelog -[2.8.0] - 2022-03-21 +[2.9.0-beta.1] - 2022-05-17 + +### Added +- Improved support (still in Beta) for Chrome's Web Serial API on ChromeOS. Other operating systems should not be affected. +- Added support for "Arduino RP2040 Connect" board +- Simplified the communication with the Web Serial API via a messaging system which simulates + 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`). +[2.8.0] - 2022-03-21 ### Added - Added support (still in Beta) for Chrome's Web Serial API on ChromeOS. Other operating systems should not be affected. diff --git a/package-lock.json b/package-lock.json index 107f1ee..21d4630 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "arduino-create-agent-js-client", - "version": "2.8.0-beta1", + "version": "2.9.0-beta.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 2634fc6..060002a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "arduino-create-agent-js-client", - "version": "2.8.0", + "version": "2.9.0-beta.1", "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/daemon.js b/src/daemon.js index 49831a6..6fcfbd8 100644 --- a/src/daemon.js +++ b/src/daemon.js @@ -32,7 +32,6 @@ export default class Daemon { this.BOARDS_URL = boardsUrl; this.UPLOAD_NOPE = 'UPLOAD_NOPE'; this.UPLOAD_DONE = 'UPLOAD_DONE'; - this.CDC_RESET_DONE = 'CDC_RESET_DONE'; this.UPLOAD_ERROR = 'UPLOAD_ERROR'; this.UPLOAD_IN_PROGRESS = 'UPLOAD_IN_PROGRESS'; @@ -56,15 +55,6 @@ export default class Daemon { this.uploadingDone = this.uploading.pipe(filter(upload => upload.status === this.UPLOAD_DONE)) .pipe(first()) .pipe(takeUntil(this.uploading.pipe(filter(upload => upload.status === this.UPLOAD_ERROR)))); - this.cdcResetDone = this.uploading.pipe( - filter(upload => upload.status === this.CDC_RESET_DONE), - first(), - takeUntil( - this.uploading.pipe( - filter(upload => upload.status === this.UPLOAD_ERROR || upload.status === this.UPLOAD_DONE)) - ) - ); - this.uploadingError = this.uploading.pipe(filter(upload => upload.status === this.UPLOAD_ERROR)) .pipe(first()) .pipe(takeUntil(this.uploadingDone)); @@ -118,12 +108,6 @@ export default class Daemon { }); } - // eslint-disable-next-line class-methods-use-this - cdcReset() { - // It's a no-op for daemons different from web serial deamon - return Promise.resolve(true); - } - /** * Upload a sketch to serial target * Fetch commandline from boards API for serial upload diff --git a/src/web-serial-daemon.js b/src/web-serial-daemon.js index 3ff77a3..d42d8ea 100644 --- a/src/web-serial-daemon.js +++ b/src/web-serial-daemon.js @@ -1,6 +1,7 @@ import { - filter, takeUntil + distinctUntilChanged, filter, takeUntil } from 'rxjs/operators'; + import Daemon from './daemon'; /** @@ -8,62 +9,176 @@ import Daemon from './daemon'; * At the moment it doesn't implement all the features available in the Chrome App Deamon * Use at your own risk. * - * The `uploader` parameter in the constructor is the component which is + * The `channel` parameter in the constructor is the component which is * used to interact with the Web Serial API. - * It must provide a method `upload`. + * + * It must provide a `postMessage` method, similarly to the object created with `chrome.runtime.connect` in + * the `chrome-app-daemon.js` module, which is used to send messages to interact with the Web Serial API. */ export default class WebSerialDaemon extends Daemon { - constructor(boardsUrl, uploader) { + constructor(boardsUrl, channel) { super(boardsUrl); + this.port = null; - this.agentFound.next(true); this.channelOpenStatus.next(true); - this.uploader = uploader; + this.channel = channel; // channel is injected from the client app + this.connectedPorts = []; + + this.init(); + } + + init() { + this.agentFound + .pipe(distinctUntilChanged()) + .subscribe(found => { + if (!found) { + // Set channelOpen false for the first time + if (this.channelOpen.getValue() === null) { + this.channelOpen.next(false); + } - this._populateSupportedBoards(); + this.connectToChannel(); + } + else { + this.openChannel(() => this.channel.postMessage({ + command: 'listPorts' + })); + } + }); } - _populateSupportedBoards() { - const supportedBoards = this.uploader.getSupportedBoards(); - this.appMessages.next({ supportedBoards }); + connectToChannel() { + this.channel.onMessage(message => { + if (message.version) { + this.agentInfo = message.version; + this.agentFound.next(true); + this.channelOpen.next(true); + } + else { + this.appMessages.next(message); + } + }); + this.channel.onDisconnect(() => { + this.channelOpen.next(false); + this.agentFound.next(false); + }); } - // eslint-disable-next-line class-methods-use-this - closeSerialMonitor() { - // TODO: it's a NO OP at the moment + _appConnect() { + this.channel.onMessage(message => { + if (message.version) { + this.agentInfo = { + version: message.version, + os: 'ChromeOS' + }; + this.agentFound.next(true); + this.channelOpen.next(true); + } + else { + this.appMessages.next(message); + } + }); + this.channel.onDisconnect(() => { + this.channelOpen.next(false); + this.agentFound.next(false); + }); } handleAppMessage(message) { if (message.ports) { + this.handleListMessage(message); + } + else if (message.supportedBoards) { + this.supportedBoards.next(message.supportedBoards); + } + if (message.serialData) { + this.serialMonitorMessages.next(message.serialData); + } + + if (message.uploadStatus) { + this.handleUploadMessage(message); + } + + if (message.err) { + this.uploading.next({ status: this.UPLOAD_ERROR, err: message.Err }); + } + } + + handleUploadMessage(message) { + if (this.uploading.getValue().status !== this.UPLOAD_IN_PROGRESS) { + return; + } + switch (message.uploadStatus) { + case 'message': + this.uploading.next({ + status: this.UPLOAD_IN_PROGRESS, + msg: message.message, + operation: message.operation, + port: message.port + }); + break; + case 'error': + this.uploading.next({ status: this.UPLOAD_ERROR, err: message.message }); + break; + case 'success': + this.uploading.next( + { + status: this.UPLOAD_DONE, + msg: message.message, + operation: message.operation, + port: message.port + } + ); + break; + + default: + this.uploading.next({ status: this.UPLOAD_IN_PROGRESS }); + } + } + + handleListMessage(message) { + const lastDevices = this.devicesList.getValue(); + if (!Daemon.devicesListAreEquals(lastDevices.serial, message.ports)) { this.devicesList.next({ - serial: message.ports, + serial: message.ports + .map(port => ({ + Name: port.name, + SerialNumber: port.serialNumber, + IsOpen: port.isOpen, + VendorID: port.vendorId, + ProductID: port.productId + })), network: [] }); - // this.handleListMessage(message); - } - - if (message.supportedBoards) { - this.supportedBoards.next(message.supportedBoards); } } /** * Send 'close' command to all the available serial ports */ - // eslint-disable-next-line class-methods-use-this closeAllPorts() { - console.log('should be closing serial ports here'); + const devices = this.devicesList.getValue().serial; + if (Array.isArray(devices)) { + devices.forEach(device => { + this.channel.postMessage({ + command: 'closePort', + data: { + name: device.Name + } + }); + }); + } } /** * Request serial port open * @param {string} port the port name */ - openSerialMonitor(port) { + openSerialMonitor(port, baudrate) { if (this.serialMonitorOpened.getValue()) { return; } - const serialPort = this.devicesList.getValue().serial[0]; // .find(p => p.Name === port); + const serialPort = this.devicesList.getValue().serial.find(p => p.Name === port); if (!serialPort) { return this.serialMonitorError.next(`Can't find port ${port}`); } @@ -77,30 +192,90 @@ export default class WebSerialDaemon extends Daemon { this.serialMonitorError.next(`Failed to open serial ${port}`); } }); - + this.channel.postMessage({ + command: 'openPort', + data: { + name: port, + baudrate + } + }); } - cdcReset({ fqbn }) { - return this.uploader.cdcReset({ fqbn }) - .then(() => { - this.uploading.next({ status: this.CDC_RESET_DONE, msg: 'Touch operation succeeded' }); - }) - .catch(error => { - this.notifyUploadError(error.message); + closeSerialMonitor(port) { + if (!this.serialMonitorOpened.getValue()) { + return; + } + const serialPort = this.devicesList.getValue().serial.find(p => p.Name === port); + if (!serialPort) { + return this.serialMonitorError.next(`Can't find port ${port}`); + } + this.appMessages + .pipe(takeUntil(this.serialMonitorOpened.pipe(filter(open => !open)))) + .subscribe(message => { + if (message.portCloseStatus === 'success') { + this.serialMonitorOpened.next(false); + } + if (message.portCloseStatus === 'error') { + this.serialMonitorError.next(`Failed to close serial ${port}`); + } }); + this.channel.postMessage({ + command: 'closePort', + data: { + name: port + } + }); + } + + cdcReset({ fqbn, port }) { + this.uploading.next({ status: this.UPLOAD_IN_PROGRESS, msg: 'CDC reset started' }); + this.channel.postMessage({ + command: 'cdcReset', + data: { + fqbn, + port + } + }); + } + + connectToSerialDevice({ fqbn }) { + this.uploading.next({ status: this.UPLOAD_IN_PROGRESS, msg: 'Board selection started' }); + this.channel.postMessage({ + command: 'connectToSerial', + data: { + fqbn + } + }); } /** * @param {object} uploadPayload * TODO: document param's shape */ - _upload(uploadPayload) { - return this.uploader.upload(uploadPayload) - .then(() => { - this.uploading.next({ status: this.UPLOAD_DONE, msg: 'Sketch uploaded' }); - }) - .catch(error => { - this.notifyUploadError(error.message); + _upload(uploadPayload, uploadCommandInfo) { + const { + board, port, commandline, data, pid, vid + } = uploadPayload; + const extrafiles = uploadCommandInfo && uploadCommandInfo.files && Array.isArray(uploadCommandInfo.files) ? uploadCommandInfo.files : []; + try { + window.oauth.getAccessToken().then(token => { + this.channel.postMessage({ + command: 'upload', + data: { + board, + port, + commandline, + data, + token: token.token, + extrafiles, + pid, + vid + } + }); }); + } + catch (err) { + this.uploading.next({ status: this.UPLOAD_ERROR, err: 'you need to be logged in on a Create site to upload by Chrome App' }); + } } }