diff --git a/package.json b/package.json index 65ac4278..b2bef37b 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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": { diff --git a/src/app/modules/components/header/header.component.ts b/src/app/modules/components/header/header.component.ts index 4dafd604..d5516aac 100644 --- a/src/app/modules/components/header/header.component.ts +++ b/src/app/modules/components/header/header.component.ts @@ -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() { diff --git a/src/app/modules/core/dialogs/debug-information/debug-information.dialog.ts b/src/app/modules/core/dialogs/debug-information/debug-information.dialog.ts index b92e3c24..4750bac2 100644 --- a/src/app/modules/core/dialogs/debug-information/debug-information.dialog.ts +++ b/src/app/modules/core/dialogs/debug-information/debug-information.dialog.ts @@ -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({ @@ -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'; diff --git a/src/app/modules/core/dialogs/upload/upload.dialog.ts b/src/app/modules/core/dialogs/upload/upload.dialog.ts index 4ee58f57..a5db487d 100644 --- a/src/app/modules/core/dialogs/upload/upload.dialog.ts +++ b/src/app/modules/core/dialogs/upload/upload.dialog.ts @@ -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) diff --git a/src/app/services/arduino-uploader/ArduinoUploader.ts b/src/app/services/arduino-uploader/ArduinoUploader.ts index d836f063..17f35e51 100644 --- a/src/app/services/arduino-uploader/ArduinoUploader.ts +++ b/src/app/services/arduino-uploader/ArduinoUploader.ts @@ -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 @@ -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') @@ -56,7 +49,7 @@ class Arduino { this.port = port } - setPort(port: SerialPort) { + setPort(port: LeaphyPort) { this.port = port } @@ -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) @@ -167,14 +160,6 @@ class Arduino { async close(): Promise { 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 diff --git a/src/app/services/arduino-uploader/protocols/avrdude/index.ts b/src/app/services/arduino-uploader/protocols/avrdude/index.ts index f712a1d6..c9f86435 100644 --- a/src/app/services/arduino-uploader/protocols/avrdude/index.ts +++ b/src/app/services/arduino-uploader/protocols/avrdude/index.ts @@ -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 }) diff --git a/src/app/services/arduino-uploader/protocols/base.ts b/src/app/services/arduino-uploader/protocols/base.ts index 26a37b81..792a7b8f 100644 --- a/src/app/services/arduino-uploader/protocols/base.ts +++ b/src/app/services/arduino-uploader/protocols/base.ts @@ -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 @@ -15,7 +16,13 @@ export default class BaseProtocol { } waitForPort() { - return new Promise((resolve, reject) => { + return new Promise(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) { @@ -23,7 +30,7 @@ export default class BaseProtocol { reject('Failed to reconnect') } - const [port] = await navigator.serial.getPorts() + const port = await this.robotWiredState.requestSerialPort(false, false) if (port) { clearInterval(interval) resolve(port) diff --git a/src/app/services/python-uploader/PythonUploader.service.ts b/src/app/services/python-uploader/PythonUploader.service.ts index d3698ea6..d906bd04 100644 --- a/src/app/services/python-uploader/PythonUploader.service.ts +++ b/src/app/services/python-uploader/PythonUploader.service.ts @@ -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"; @@ -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(); @@ -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') diff --git a/src/app/services/python-uploader/mip/PackageManager.ts b/src/app/services/python-uploader/mip/PackageManager.ts index ee7b630f..ef74af8c 100644 --- a/src/app/services/python-uploader/mip/PackageManager.ts +++ b/src/app/services/python-uploader/mip/PackageManager.ts @@ -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; } diff --git a/src/app/state/robot.wired.state.ts b/src/app/state/robot.wired.state.ts index 2c53c3eb..3987cc6f 100644 --- a/src/app/state/robot.wired.state.ts +++ b/src/app/state/robot.wired.state.ts @@ -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({ @@ -11,7 +15,7 @@ export class RobotWiredState { public SUPPORTED_VENDORS = [0x1a86, 9025, 2341, 0x0403, 0x2e8a] - private serialPortSubject$: BehaviorSubject = new BehaviorSubject(null); + private serialPortSubject$: BehaviorSubject = new BehaviorSubject(null); private abortControllerSubject$: BehaviorSubject = new BehaviorSubject(null); @@ -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(); } diff --git a/src/environments/environment.dev.ts b/src/environments/environment.dev.ts new file mode 100644 index 00000000..8d95ecc9 --- /dev/null +++ b/src/environments/environment.dev.ts @@ -0,0 +1,4 @@ +export const environment = { + production: false, + backend: 'https://testleaphyeasybloqs.com:8443', +}; diff --git a/tsconfig.json b/tsconfig.json index 3fad718a..d8f2dd4b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compileOnSave": false, "compilerOptions": { + "skipLibCheck": true, "baseUrl": "./", "outDir": "./dist/out-tsc", "sourceMap": true, diff --git a/yarn.lock b/yarn.lock index a1949df5..1c77e399 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"