Skip to content

Implement support for RP2040 on Chromebook PWA #546

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v14.16.0
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
16 changes: 0 additions & 16 deletions src/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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));
Expand Down Expand Up @@ -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
Expand Down
251 changes: 213 additions & 38 deletions src/web-serial-daemon.js
Original file line number Diff line number Diff line change
@@ -1,69 +1,184 @@
import {
filter, takeUntil
distinctUntilChanged, filter, takeUntil
} from 'rxjs/operators';

import Daemon from './daemon';

/**
* WARNING: the WebSerialDaemon with support for the Web Serial API is still in an alpha version.
* 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}`);
}
Expand All @@ -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' });
}
}
}