Skip to content
This repository has been archived by the owner on Aug 6, 2024. It is now read-only.

Commit

Permalink
feat: add WebUSB fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
sverben authored and rmoesbergen committed Apr 3, 2024
1 parent 480518b commit a08d8a0
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 57 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"main": "src/main.ts",
"scripts": {
"ng": "ng",
"start": "ng serve --open",
"start": "ng serve --open --configuration dev",
"build": "ng build --configuration production --base-href ./",
"buildLocal": "ng build --configuration local --base-href ./",
"buildstart": "ng build --base-href ./ && yarn run start",
Expand Down Expand Up @@ -59,6 +59,7 @@
"rxjs": "~7.5.5",
"showdown": "^2.1.0",
"tslib": "^2.4.0",
"web-serial-polyfill": "^1.0.15",
"zone.js": "~0.14.3"
},
"devDependencies": {
Expand Down
30 changes: 12 additions & 18 deletions src/app/modules/components/header/header.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,25 +77,19 @@ export class HeaderComponent {
}

public async onChooseRobot() {
if ('serial' in navigator) {
const port = await navigator.serial.requestPort({
filters: this.robotWiredState.SUPPORTED_VENDORS.map(vendor => ({
usbVendorId: vendor
}))
})
if (port !== this.robotWiredState.getSerialPort()) {
await port.open({baudRate: 115200});
this.robotWiredState.setSerialPort(port);
this.dialogState.setIsSerialOutputListening(true);
}

this.snackBar.openFromComponent(StatusMessageDialog, {
duration: 2000,
horizontalPosition: 'center',
verticalPosition: 'bottom',
data: { message: "CONNECTED" }
})
const port = await this.robotWiredState.requestSerialPort(true)
if (port !== this.robotWiredState.getSerialPort()) {
await port.open({baudRate: 115200});
this.robotWiredState.setSerialPort(port);
this.dialogState.setIsSerialOutputListening(true);
}

this.snackBar.openFromComponent(StatusMessageDialog, {
duration: 2000,
horizontalPosition: 'center',
verticalPosition: 'bottom',
data: { message: "CONNECTED" }
})
}

public onSaveWorkspaceClicked() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Component} from '@angular/core';
import {MatDialogRef} from '@angular/material/dialog';
import {RobotWiredState} from "../../../../state/robot.wired.state";
import {LeaphyPort, RobotWiredState} from "../../../../state/robot.wired.state";
import {TranslateService} from "@ngx-translate/core";

@Component({
Expand All @@ -13,7 +13,7 @@ export class DebugInformationDialog {
public os: string = '';
public browser: string = '';
public browserVersion: string = '';
public serialPort: SerialPort | undefined;
public serialPort?: LeaphyPort;
public webSerialSupported: string = 'Supported';


Expand Down
2 changes: 1 addition & 1 deletion src/app/modules/core/dialogs/upload/upload.dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class UploadDialog {

public async startUpload(source_code: string, board: string, libraries: string) {
this.dialogState.setIsSerialOutputListening(false);
if (!('serial' in navigator)) {
if (!navigator.serial && !navigator.usb) {
this.uploadState.setStatusMessage('NO_SERIAL_SUPPORT')
this.uploadState.setFailed(true)
this.uploadState.setDone(true)
Expand Down
27 changes: 6 additions & 21 deletions src/app/services/arduino-uploader/ArduinoUploader.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {clearReadBuffer, delay} from './utils'
import {RobotWiredState} from "../../state/robot.wired.state";
import {LeaphyPort, RobotWiredState} from "../../state/robot.wired.state";
import {AppState} from "../../state/app.state";
import {UploadState} from "../../state/upload.state";

class Arduino {
port: SerialPort = null
port: LeaphyPort = null
isUploading = false
serialOptions = null
readStream = null
Expand All @@ -31,16 +31,9 @@ class Arduino {
* Open a connection to a user-selected Arduino.
*/
async connect() {
let port: SerialPort;
let port: LeaphyPort;
try {
const ports = await navigator.serial.getPorts()

if (ports[0]) port = ports[0]
else port = await navigator.serial.requestPort({
filters: this.robotWiredState.SUPPORTED_VENDORS.map(vendor => ({
usbVendorId: vendor
}))
})
port = await this.robotWiredState.requestSerialPort(false)
} catch (error) {
console.log(error)
throw new Error('No device selected')
Expand All @@ -56,7 +49,7 @@ class Arduino {
this.port = port
}

setPort(port: SerialPort) {
setPort(port: LeaphyPort) {
this.port = port
}

Expand Down Expand Up @@ -112,7 +105,7 @@ class Arduino {
.catch(async () => {
await delay(4000)

const [port] = await navigator.serial.getPorts()
const port = await this.robotWiredState.requestSerialPort(false)
if (!port) {
this.port = null
this.robotWiredState.setSerialPort(null)
Expand Down Expand Up @@ -167,14 +160,6 @@ class Arduino {
async close(): Promise<void> {
this.isUploading = false
}

/**
* Check if web-serial is available.
* @returns {boolean} True if web-serial is available.
*/
static isAvailable(): boolean {
return 'serial' in navigator
}
}

export default Arduino
3 changes: 3 additions & 0 deletions src/app/services/arduino-uploader/protocols/avrdude/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export default class Avrdude extends BaseProtocol {

// create a promise that resolves when the port.ondisconnect event is fired
const disconnectPromise = new Promise((resolve) => {
// todo: add compatibility for fallback devices if required, currently no avrdude devices support this
if (!(this.port instanceof SerialPort)) return

this.port.ondisconnect = resolve
})

Expand Down
15 changes: 11 additions & 4 deletions src/app/services/arduino-uploader/protocols/base.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {RobotWiredState} from "../../../state/robot.wired.state";
import {LeaphyPort, RobotWiredState} from "../../../state/robot.wired.state";
import ArduinoUploader from "../ArduinoUploader";
import {UploadState} from "../../../state/upload.state";
import {SerialPort} from "web-serial-polyfill";

export default class BaseProtocol {
constructor(
public port: SerialPort,
public port: LeaphyPort,
public robotWiredState: RobotWiredState,
public uploadState: UploadState,
public uploader: ArduinoUploader
Expand All @@ -15,15 +16,21 @@ export default class BaseProtocol {
}

waitForPort() {
return new Promise<SerialPort>((resolve, reject) => {
return new Promise<LeaphyPort>(async (resolve, reject) => {
const platform = navigator.userAgent.toLowerCase()
if (platform.includes('android')) {
const device = await this.uploadState.requestUSBDevice()
resolve(new SerialPort(device))
}

let attempts = 0
let interval = setInterval(async () => {
if (++attempts > 200) {
clearInterval(interval)
reject('Failed to reconnect')
}

const [port] = await navigator.serial.getPorts()
const port = await this.robotWiredState.requestSerialPort(false, false)
if (port) {
clearInterval(interval)
resolve(port)
Expand Down
8 changes: 4 additions & 4 deletions src/app/services/python-uploader/PythonUploader.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {RobotWiredState} from "../../state/robot.wired.state";
import {LeaphyPort, RobotWiredState} from "../../state/robot.wired.state";
import {sendCommand, enterReplMode, readResponse} from "./comms/BoardCommunication";
import {put, get, ls, rm, rmdir} from "./filesystem/FileSystem";
import {Injectable} from "@angular/core";
Expand All @@ -9,7 +9,7 @@ import {PackageManager} from "./mip/PackageManager";
})
export class PythonUploaderService {
drive: FileSystemDirectoryHandle = null
port: SerialPort = null
port: LeaphyPort = null
private firmware: Blob = null
private packageManager: PackageManager = new PackageManager();

Expand Down Expand Up @@ -50,9 +50,9 @@ export class PythonUploaderService {
async connect() {
await this.setPythonCodeRunning(false);
this.robotWiredState.addToUploadLog('Connecting to device');
let port: SerialPort;
let port: LeaphyPort;
try {
port = await navigator.serial.requestPort({filters: [{usbVendorId: 11914}, {usbVendorId: 9025}]});
port = await this.robotWiredState.requestSerialPort(false);
} catch (error) {
console.log(error)
throw new Error('No device selected')
Expand Down
7 changes: 4 additions & 3 deletions src/app/services/python-uploader/mip/PackageManager.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import {get, ls, mkdir, put, rm, rmdir} from "../filesystem/FileSystem";
import {environment} from "src/environments/environment";
import {LeaphyPort} from "../../../state/robot.wired.state";


export class PackageManager {

private serialPort: SerialPort;
private serialPort: LeaphyPort;

constructor() {}

set port(port: SerialPort) {
set port(port: LeaphyPort) {
this.serialPort = port;
}

get port(): SerialPort {
get port(): LeaphyPort {
return this.serialPort;
}

Expand Down
49 changes: 46 additions & 3 deletions src/app/state/robot.wired.state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { Injectable } from '@angular/core';
import { ChartDataset } from 'chart.js';
import { ReplaySubject, BehaviorSubject, Observable} from 'rxjs';
import { filter, map, scan } from 'rxjs/operators';
import { SerialPort as MockedSerialPort } from "web-serial-polyfill";


export type LeaphyPort = SerialPort|MockedSerialPort


@Injectable({
Expand All @@ -11,7 +15,7 @@ export class RobotWiredState {

public SUPPORTED_VENDORS = [0x1a86, 9025, 2341, 0x0403, 0x2e8a]

private serialPortSubject$: BehaviorSubject<SerialPort> = new BehaviorSubject(null);
private serialPortSubject$: BehaviorSubject<LeaphyPort> = new BehaviorSubject(null);

private abortControllerSubject$: BehaviorSubject<AbortController> = new BehaviorSubject(null);

Expand Down Expand Up @@ -85,11 +89,50 @@ export class RobotWiredState {
this.setIncomingSerialData({ time: new Date(), data: this.poisonPill });
}

public setSerialPort(port: SerialPort): void {
public async requestSerialPort(forcePrompt = false, allowPrompt = true) {
let port: LeaphyPort

if (navigator.serial) {
if (!forcePrompt) {
const ports = await navigator.serial.getPorts()
if (ports[0]) port = ports[0]
}
if (!port && allowPrompt) {
port = await navigator.serial.requestPort({
filters: this.SUPPORTED_VENDORS.map(vendor => ({
usbVendorId: vendor
}))
})
}
} else if (navigator.usb) {
if (!forcePrompt) {
const devices = await navigator.usb.getDevices()
if (devices[0]) port = new MockedSerialPort(devices[0])
}
if (!port && allowPrompt) {
const device = await navigator.usb.requestDevice({
filters: this.SUPPORTED_VENDORS.map(vendor => ({
vendorId: vendor
}))
})
try {
port = new MockedSerialPort(device)
} catch {
throw new Error("WebUSB device is not supported")
}
}
} else {
throw new Error("WebSerial/WebUSB not supported")
}

return port
}

public setSerialPort(port: LeaphyPort): void {
this.serialPortSubject$.next(port);
}

public getSerialPort(): SerialPort {
public getSerialPort(): LeaphyPort {
return this.serialPortSubject$.getValue();
}

Expand Down
4 changes: 4 additions & 0 deletions src/environments/environment.dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const environment = {
production: false,
backend: 'https://testleaphyeasybloqs.com:8443',
};
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"compileOnSave": false,
"compilerOptions": {
"skipLibCheck": true,
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9578,6 +9578,11 @@ wcwidth@^1.0.1:
dependencies:
defaults "^1.0.3"

web-serial-polyfill@^1.0.15:
version "1.0.15"
resolved "https://registry.yarnpkg.com/web-serial-polyfill/-/web-serial-polyfill-1.0.15.tgz#0c0b4cd5f32b8c07243d2ffeea484892f46986b1"
integrity sha512-usZN7kGRkEWr8DzRWxW+og55L1fHo4hNIwxCSCfWKpM+i0L+2AwzupMvkDFxnJNqUFOhLaD3PlgAOJxUOUrAoA==

webdriver-js-extender@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz#57d7a93c00db4cc8d556e4d3db4b5db0a80c3bb7"
Expand Down

0 comments on commit a08d8a0

Please sign in to comment.