diff --git a/package-lock.json b/package-lock.json index 3ff62159d..6cdebd3bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@codemirror/view": "^6.26.3", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@microbit/microbit-connection": "^0.0.0-alpha.36", + "@microbit/microbit-connection": "^0.9.0-apps.alpha.17", "@microbit/microbit-fs": "^0.10.0", "@sanity/block-content-to-react": "^3.0.0", "@sanity/image-url": "^1.0.1", @@ -1723,6 +1723,48 @@ "specificity": "bin/cli.js" } }, + "node_modules/@capacitor-community/bluetooth-le": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@capacitor-community/bluetooth-le/-/bluetooth-le-7.3.2.tgz", + "integrity": "sha512-7dgtglFXGmyS849XtIZ62iA997aE3yqNsq7XsC8yjR8dqe/agh852+nW1Zk/8QlG58dPu/K5fxVzlWZvZFb80g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.20" + }, + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, + "node_modules/@capacitor/core": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.6.0.tgz", + "integrity": "sha512-K6LEvgxW6Nd6uNWHj3U512hUtB4kln71TvVgdvwJRFMLySpUrWwb/A8Q7c3QGogvPvgOjN1U65onkyRTihPGwA==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@capacitor/filesystem": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-7.1.8.tgz", + "integrity": "sha512-Qpw/2SE4/CzqAUvGgSM9hw/uXQ5qoOaF4wxbToXwpAaKPS+tzletS1h5ti3jjLmGcqizTs2sEXMtcsARW/Ceew==", + "license": "MIT", + "peer": true, + "dependencies": { + "@capacitor/synapse": "^1.0.3" + }, + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, + "node_modules/@capacitor/synapse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz", + "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==", + "license": "ISC" + }, "node_modules/@chakra-ui/accordion": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@chakra-ui/accordion/-/accordion-2.3.1.tgz", @@ -4128,16 +4170,34 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@microbit/capacitor-community-nordic-dfu": { + "version": "7.0.0-microbit.4", + "resolved": "https://registry.npmjs.org/@microbit/capacitor-community-nordic-dfu/-/capacitor-community-nordic-dfu-7.0.0-microbit.4.tgz", + "integrity": "sha512-ffCmbAqvBrd6kSyNu80wh51LlBfoy3UbWsMUjnPHYlTKh/tthi5efDbuRBJUfT2E/4ZznrAPN2wuwLMBcah/CA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@capacitor/core": "^7.0.0" + } + }, "node_modules/@microbit/microbit-connection": { - "version": "0.0.0-alpha.36", - "resolved": "https://registry.npmjs.org/@microbit/microbit-connection/-/microbit-connection-0.0.0-alpha.36.tgz", - "integrity": "sha512-R/UQmPQq1s9eSx1NFWPfsJAsflJVbMCyfuHAuqEri3eCSRy5cJ8qqRmdlMQ40XfTps3roDMJkVUGN5Zw6JUCAg==", + "version": "0.9.0-apps.alpha.17", + "resolved": "https://registry.npmjs.org/@microbit/microbit-connection/-/microbit-connection-0.9.0-apps.alpha.17.tgz", + "integrity": "sha512-XPMeItIEjhXAIYgUsJl2OEaKwHroNqu2CshW0VFApOmW/WGY82eJ/ml3oQ1ZU2xvJ/bmpBVRa3YOpJLJim/lTQ==", "dependencies": { "@microbit/microbit-universal-hex": "^0.2.2", + "@types/w3c-web-usb": "^1.0.13", "@types/web-bluetooth": "^0.0.20", - "crelt": "^1.0.6", - "dapjs": "^2.3.0", "nrf-intel-hex": "^1.4.0" + }, + "peerDependencies": { + "@capacitor-community/bluetooth-le": "^7.3.0", + "@capacitor/core": "^7.4.4", + "@capacitor/filesystem": "^7.1.6", + "@microbit/capacitor-community-nordic-dfu": "v7.0.0-microbit.4" } }, "node_modules/@microbit/microbit-fs": { @@ -5380,14 +5440,6 @@ "form-data": "^4.0.0" } }, - "node_modules/@types/node-hid": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@types/node-hid/-/node-hid-1.3.4.tgz", - "integrity": "sha512-0ootpsYetN9vjqkDSwm/cA4fk/9yGM/PO0X8SLPE/BzXlUaBelImMWMymtF9QEoEzxY0pnhcROIJM0CNSUqO8w==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node/node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -5450,18 +5502,11 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "devOptional": true }, - "node_modules/@types/usb": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/usb/-/usb-1.5.4.tgz", - "integrity": "sha512-NOUza/8yuswu6RoECQyPHEjA34qpDaeONQ72fm+bCnnN2DJjDePAY+NsmV17H88oIlq4JlJ2mD5Kh5d6R2MwTQ==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/w3c-web-usb": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.10.tgz", - "integrity": "sha512-CHgUI5kTc/QLMP8hODUHhge0D4vx+9UiAwIGiT0sTy/B2XpdX1U5rJt6JSISgr6ikRT7vxV9EVAFeYZqUnl1gQ==" + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.13.tgz", + "integrity": "sha512-N2nSl3Xsx8mRHZBvMSdNGtzMyeleTvtlEw+ujujgXalPqOjIA6UtrqcB6OzyUjkTbDm3J7P1RNK1lgoO7jxtsw==", + "license": "MIT" }, "node_modules/@types/web-bluetooth": { "version": "0.0.20", @@ -7008,19 +7053,6 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, - "node_modules/dapjs": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/dapjs/-/dapjs-2.3.0.tgz", - "integrity": "sha512-quanzq7+2xnqgGqqYgARz9o3iBcZ3Ir5r5mTA7WPsjrp9ilEqqCToSFGTL+8HuGP35dUIL7O+yMBloYHhHgZDA==", - "dependencies": { - "@types/node-hid": "^1.2.0", - "@types/usb": "^1.5.1", - "@types/w3c-web-usb": "^1.0.4" - }, - "engines": { - "node": ">=8.14.0" - } - }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", diff --git a/package.json b/package.json index 235e77d68..01c2bd62f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@codemirror/view": "^6.26.3", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@microbit/microbit-connection": "^0.0.0-alpha.36", + "@microbit/microbit-connection": "^0.9.0-apps.alpha.17", "@microbit/microbit-fs": "^0.10.0", "@sanity/block-content-to-react": "^3.0.0", "@sanity/image-url": "^1.0.1", diff --git a/src/App.tsx b/src/App.tsx index 813fea79d..020de3f2a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,7 +10,7 @@ import "./App.css"; import { DialogProvider } from "./common/use-dialogs"; import VisualViewPortCSSVariables from "./common/VisualViewportCSSVariables"; import { deployment, useDeployment } from "./deployment"; -import { createWebUSBConnection } from "@microbit/microbit-connection"; +import { createUSBConnection } from "@microbit/microbit-connection/usb"; import { DeviceContextProvider } from "./device/device-hooks"; import { MockDeviceConnection } from "./device/mock"; import DocumentationProvider from "./documentation/documentation-hooks"; @@ -40,7 +40,7 @@ const isMockDeviceMode = () => const logging = deployment.logging; const device = isMockDeviceMode() ? new MockDeviceConnection() - : createWebUSBConnection({ logging }); + : createUSBConnection({ logging }); const host = createHost(logging); const fs = new FileSystem(logging, host, fetchMicroPython); diff --git a/src/common/ProgressDialog.tsx b/src/common/ProgressDialog.tsx index 72661a90f..146c28690 100644 --- a/src/common/ProgressDialog.tsx +++ b/src/common/ProgressDialog.tsx @@ -19,7 +19,7 @@ const doNothing = () => {}; export interface ProgressDialogParameters { header: ReactNode; body?: ReactNode; - progress: number | undefined; + progress?: number; } interface ProgressDialogProps extends ProgressDialogParameters { @@ -29,10 +29,15 @@ interface ProgressDialogProps extends ProgressDialogParameters { /** * A progress dialog used for the flashing process. */ -const ProgressDialog = ({ header, body, progress }: ProgressDialogProps) => { +const ProgressDialog = ({ + isOpen, + header, + body, + progress, +}: ProgressDialogProps) => { return ( { alignItems="flex-start" > {body} - + diff --git a/src/common/use-dialogs.tsx b/src/common/use-dialogs.tsx index 5813bc8fc..9d5dbf7fa 100644 --- a/src/common/use-dialogs.tsx +++ b/src/common/use-dialogs.tsx @@ -65,11 +65,11 @@ export class Dialogs { } progress(options: ProgressDialogParameters): void { - if (options.progress === undefined) { - this.progressDialogSetState(undefined); - } else { - this.progressDialogSetState(options); - } + this.progressDialogSetState(options); + } + + closeProgress(): void { + this.progressDialogSetState(undefined); } } diff --git a/src/deployment/default/index.tsx b/src/deployment/default/index.tsx index 658d681fe..ba77942ab 100644 --- a/src/deployment/default/index.tsx +++ b/src/deployment/default/index.tsx @@ -5,7 +5,7 @@ */ import { ReactNode, createContext } from "react"; import { CookieConsent, DeploymentConfigFactory } from ".."; -import { NullLogging } from "./logging"; +import { ConsoleLogging } from "./logging"; import theme from "./theme"; const stubConsentValue: CookieConsent = { @@ -20,7 +20,7 @@ const defaultDeploymentFactory: DeploymentConfigFactory = () => ({ chakraTheme: theme, // This isn't ideal as it's the branded version. You can just remove the field to remove the welcome dialog. welcomeVideoYouTubeId: "mREwMW69qKc", - logging: new NullLogging(), + logging: new ConsoleLogging(), compliance: { ConsentProvider: ({ children }: { children: ReactNode }) => ( diff --git a/src/deployment/default/logging.ts b/src/deployment/default/logging.ts index 5b0ca8ce9..b71a88029 100644 --- a/src/deployment/default/logging.ts +++ b/src/deployment/default/logging.ts @@ -5,8 +5,14 @@ */ import { Event, Logging } from "../../logging/logging"; -export class NullLogging implements Logging { - event(_event: Event): void {} - error(_e: any): void {} - log(_e: any): void {} +export class ConsoleLogging implements Logging { + event(event: Event): void { + console.log(event); + } + error(e: any): void { + console.error(e); + } + log(e: any): void { + console.log(e); + } } diff --git a/src/device/device-hooks.tsx b/src/device/device-hooks.tsx index c85fe1dbc..fa070a63b 100644 --- a/src/device/device-hooks.tsx +++ b/src/device/device-hooks.tsx @@ -15,13 +15,15 @@ import { useFileSystem } from "../fs/fs-hooks"; import { useLogging } from "../logging/logging-hooks"; import { ConnectionStatus, - MicrobitWebUSBConnection, - SerialDataEvent, - ConnectionStatusEvent, + ConnectionStatusChange, } from "@microbit/microbit-connection"; +import { + MicrobitUSBConnection, + SerialData, +} from "@microbit/microbit-connection/usb"; import { SimulatorDeviceConnection } from "./simulator"; -const DeviceContext = React.createContext( +const DeviceContext = React.createContext( undefined ); @@ -56,7 +58,7 @@ export const useConnectionStatus = () => { const device = useDevice(); const [status, setStatus] = useState(device.status); useEffect(() => { - const statusListener = (event: ConnectionStatusEvent) => { + const statusListener = (event: ConnectionStatusChange) => { setStatus(event.status); }; device.addEventListener("status", statusListener); @@ -185,7 +187,7 @@ export const useDeviceTraceback = () => { useEffect(() => { const buffer = new TracebackScrollback(); - const dataListener = (event: SerialDataEvent) => { + const dataListener = (event: SerialData) => { const latest = buffer.push(event.data); setRuntimeError((current) => { if (!current && latest) { @@ -234,7 +236,7 @@ export const DeviceContextProvider = ({ value: device, children, }: { - value: MicrobitWebUSBConnection; + value: MicrobitUSBConnection; children: ReactNode; }) => { const syncStatusState = useState(SyncStatus.OUT_OF_SYNC); diff --git a/src/device/mock.ts b/src/device/mock.ts index 817a3fbc4..604c20480 100644 --- a/src/device/mock.ts +++ b/src/device/mock.ts @@ -5,18 +5,18 @@ */ import { BoardVersion, + ConnectionAvailabilityStatus, ConnectionStatus, DeviceConnectionEventMap, FlashDataSource, - FlashEvent, - SerialDataEvent, - ConnectionStatusEvent, + FlashOptions, + ProgressStage, DeviceError, DeviceErrorCode, TypedEventTarget, - MicrobitWebUSBConnection, - SerialConnectionEventMap, } from "@microbit/microbit-connection"; +import { SerialConnectionEventMap } from "@microbit/microbit-connection/usb"; +import { MicrobitUSBConnection } from "@microbit/microbit-connection/usb"; /** * A mock device used during end-to-end testing. @@ -27,13 +27,12 @@ import { */ export class MockDeviceConnection extends TypedEventTarget - implements MicrobitWebUSBConnection + implements MicrobitUSBConnection { - status: ConnectionStatus = (navigator as any).usb - ? ConnectionStatus.NO_AUTHORIZED_DEVICE - : ConnectionStatus.NOT_SUPPORTED; + status: ConnectionStatus = ConnectionStatus.NoAuthorizedDevice; private connectResults: DeviceErrorCode[] = []; + private availability: ConnectionAvailabilityStatus = "available"; constructor() { super(); @@ -42,7 +41,7 @@ export class MockDeviceConnection } mockSerialWrite(data: string) { - this.dispatchTypedEvent("serialdata", new SerialDataEvent(data)); + this.dispatchEvent("serialdata", { data }); } mockConnect(code: DeviceErrorCode) { @@ -50,26 +49,30 @@ export class MockDeviceConnection } async initialize(): Promise {} + async checkAvailability() { + return this.availability; + } dispose() {} - getDeviceId(): number | undefined { - return undefined; + getDeviceId(): number { + return 0; } setRequestDeviceExclusionFilters(): void {} - getDevice() {} + getDevice() { + return undefined; + } async softwareReset(): Promise {} - async connect(): Promise { + async connect(): Promise { const next = this.connectResults.shift(); if (next) { throw new DeviceError({ code: next, message: "Mocked failure" }); } - this.setStatus(ConnectionStatus.CONNECTED); - return this.status; + this.setStatus(ConnectionStatus.Connected); } - getBoardVersion(): BoardVersion | undefined { + getBoardVersion(): BoardVersion { return "V2"; } @@ -81,26 +84,16 @@ export class MockDeviceConnection */ async flash( _dataSource: FlashDataSource, - options: { - /** - * True to use a partial flash where possible, false to force a full flash. - */ - partial: boolean; - /** - * A progress callback. Called with undefined when the process is complete or has failed. - */ - progress: (percentage: number | undefined) => void; - } + options: FlashOptions ): Promise { await new Promise((resolve) => setTimeout(resolve, 100)); - options.progress(0.5); + options.progress?.(ProgressStage.PartialFlashing, 0.5); await new Promise((resolve) => setTimeout(resolve, 100)); - options.progress(undefined); - this.dispatchTypedEvent("flash", new FlashEvent()); + this.dispatchEvent("flash"); } async disconnect(): Promise { - this.setStatus(ConnectionStatus.DISCONNECTED); + this.setStatus(ConnectionStatus.Disconnected); } async serialWrite(data: string): Promise { @@ -108,15 +101,19 @@ export class MockDeviceConnection } private setStatus(newStatus: ConnectionStatus) { + const previousStatus = this.status; this.status = newStatus; - this.dispatchTypedEvent("status", new ConnectionStatusEvent(this.status)); + this.dispatchEvent("status", { + status: newStatus, + previousStatus, + }); } clearDevice(): void { - this.setStatus(ConnectionStatus.NO_AUTHORIZED_DEVICE); + this.setStatus(ConnectionStatus.NoAuthorizedDevice); } mockWebUsbNotSupported(): void { - this.setStatus(ConnectionStatus.NOT_SUPPORTED); + this.availability = "unsupported"; } } diff --git a/src/device/simulator.ts b/src/device/simulator.ts index fce601d38..06b2d1e3e 100644 --- a/src/device/simulator.ts +++ b/src/device/simulator.ts @@ -6,53 +6,25 @@ import { BoardVersion, ConnectionStatus, - ConnectionStatusEvent, DeviceConnectionEventMap, - FlashEvent, - MicrobitWebUSBConnection, - SerialConnectionEventMap, - SerialDataEvent, - SerialResetEvent, TypedEventTarget, } from "@microbit/microbit-connection"; +import { SerialConnectionEventMap } from "@microbit/microbit-connection/usb"; +import { MicrobitUSBConnection } from "@microbit/microbit-connection/usb"; import { Logging } from "../logging/logging"; // Simulator-only events. -export class LogDataEvent extends Event { - constructor(public readonly log: DataLog) { - super("log_data"); - } +export interface LogDataEvent { + log: DataLog; } -export class RadioDataEvent extends Event { - constructor(public readonly text: string) { - super("radio_data"); - } -} - -export class RadioGroupEvent extends Event { - constructor(public readonly group: number) { - super("radio_group"); - } +export interface RadioDataEvent { + text: string; } -export class RadioResetEvent extends Event { - constructor() { - super("radio_reset"); - } -} - -export class StateChangeEvent extends Event { - constructor(public readonly state: SimulatorState) { - super("state_change"); - } -} - -export class RequestFlashEvent extends Event { - constructor() { - super("request_flash"); - } +export interface StateChangeEvent { + state: SimulatorState; } // It'd be nice to publish these types from the simulator project. @@ -164,13 +136,12 @@ const initialDataLog = (): DataLog => ({ data: [], }); -class SimulatorEventMap extends DeviceConnectionEventMap { - "log_data": LogDataEvent; - "radio_data": RadioDataEvent; - "radio_group": RadioGroupEvent; - "radio_reset": RadioResetEvent; - "state_change": StateChangeEvent; - "request_flash": RequestFlashEvent; +interface SimulatorEventMap extends DeviceConnectionEventMap { + log_data: LogDataEvent; + radio_data: RadioDataEvent; + radio_reset: void; + state_change: StateChangeEvent; + request_flash: void; } /** @@ -180,9 +151,9 @@ class SimulatorEventMap extends DeviceConnectionEventMap { */ export class SimulatorDeviceConnection extends TypedEventTarget - implements MicrobitWebUSBConnection + implements MicrobitUSBConnection { - status: ConnectionStatus = ConnectionStatus.NO_AUTHORIZED_DEVICE; + status: ConnectionStatus = ConnectionStatus.NoAuthorizedDevice; state: SimulatorState | undefined; log: DataLog = initialDataLog(); @@ -197,14 +168,14 @@ export class SimulatorDeviceConnection case "ready": { const newState = event.data.state; this.state = newState; - this.dispatchTypedEvent("state_change", new StateChangeEvent(newState)); - if (this.status !== ConnectionStatus.CONNECTED) { - this.setStatus(ConnectionStatus.CONNECTED); + this.dispatchEvent("state_change", { state: newState }); + if (this.status !== ConnectionStatus.Connected) { + this.setStatus(ConnectionStatus.Connected); } break; } case "request_flash": { - this.dispatchTypedEvent("request_flash", new RequestFlashEvent()); + this.dispatchEvent("request_flash"); this.logging.event({ type: "sim-user-start", }); @@ -216,7 +187,7 @@ export class SimulatorDeviceConnection ...event.data.change, }; this.state = updated; - this.dispatchTypedEvent("state_change", new StateChangeEvent(updated)); + this.dispatchEvent("state_change", { state: updated }); break; } case "radio_output": { @@ -229,7 +200,7 @@ export class SimulatorDeviceConnection // eslint-disable-next-line no-control-regex .replace(/^\x01\x00\x01/, ""); if (message instanceof Uint8Array) { - this.dispatchTypedEvent("radio_data", new RadioDataEvent(text)); + this.dispatchEvent("radio_data", { text }); } break; } @@ -247,18 +218,18 @@ export class SimulatorDeviceConnection result.data.push({ data: entry.data }); } this.log = result; - this.dispatchTypedEvent("log_data", new LogDataEvent(this.log)); + this.dispatchEvent("log_data", { log: this.log }); break; } case "log_delete": { this.log = initialDataLog(); - this.dispatchTypedEvent("log_data", new LogDataEvent(this.log)); + this.dispatchEvent("log_data", { log: this.log }); break; } case "serial_output": { const text = event.data.data; if (typeof text === "string") { - this.dispatchTypedEvent("serialdata", new SerialDataEvent(text)); + this.dispatchEvent("serialdata", { data: text }); } break; } @@ -292,19 +263,18 @@ export class SimulatorDeviceConnection async initialize(): Promise { window.addEventListener("message", this.messageListener); - this.setStatus(ConnectionStatus.DISCONNECTED); + this.setStatus(ConnectionStatus.Disconnected); } dispose() { window.removeEventListener("message", this.messageListener); } - async connect(): Promise { - this.setStatus(ConnectionStatus.CONNECTED); - return this.status; + async connect(): Promise { + this.setStatus(ConnectionStatus.Connected); } - getBoardVersion(): BoardVersion | undefined { + getBoardVersion(): BoardVersion { return "V2"; } @@ -320,7 +290,7 @@ export class SimulatorDeviceConnection filesystem, }); this.notifyResetComms(); - this.dispatchTypedEvent("flash", new FlashEvent()); + this.dispatchEvent("flash"); } configure(config: Config): void { @@ -329,13 +299,13 @@ export class SimulatorDeviceConnection private notifyResetComms() { // Might be nice to rework so this was all about connection state changes. - this.dispatchTypedEvent("serialreset", new SerialResetEvent()); - this.dispatchTypedEvent("radio_reset", new RadioResetEvent()); + this.dispatchEvent("serialreset"); + this.dispatchEvent("radio_reset"); } async disconnect(): Promise { window.removeEventListener("message", this.messageListener); - this.setStatus(ConnectionStatus.DISCONNECTED); + this.setStatus(ConnectionStatus.Disconnected); } async serialWrite(data: string): Promise { @@ -370,7 +340,7 @@ export class SimulatorDeviceConnection value, }, }; - this.dispatchTypedEvent("state_change", new StateChangeEvent(this.state)); + this.dispatchEvent("state_change", { state: this.state }); this.postMessage("set_value", { id, value, @@ -405,12 +375,16 @@ export class SimulatorDeviceConnection }; private setStatus(newStatus: ConnectionStatus) { + const previousStatus = this.status; this.status = newStatus; - this.dispatchTypedEvent("status", new ConnectionStatusEvent(newStatus)); + this.dispatchEvent("status", { + status: newStatus, + previousStatus, + }); } clearDevice(): void { - this.setStatus(ConnectionStatus.NO_AUTHORIZED_DEVICE); + this.setStatus(ConnectionStatus.NoAuthorizedDevice); } private postMessage(kind: string, data: any): void { @@ -427,11 +401,16 @@ export class SimulatorDeviceConnection ); } - getDeviceId(): number | undefined { - return undefined; + async checkAvailability() { + return "available" as const; + } + getDeviceId(): number { + return 0; } setRequestDeviceExclusionFilters(): void {} async flash(): Promise {} - getDevice() {} + getDevice() { + return undefined; + } async softwareReset(): Promise {} } diff --git a/src/editor/codemirror/language-server/diagnostics.ts b/src/editor/codemirror/language-server/diagnostics.ts index a6045addf..ae3e96b88 100644 --- a/src/editor/codemirror/language-server/diagnostics.ts +++ b/src/editor/codemirror/language-server/diagnostics.ts @@ -7,7 +7,7 @@ import { Text } from "@codemirror/state"; import * as LSP from "vscode-languageserver-protocol"; import { Action, Diagnostic } from "../lint/lint"; import { positionToOffset } from "./positions"; -import { MicrobitWebUSBConnection } from "@microbit/microbit-connection"; +import { MicrobitUSBConnection } from "@microbit/microbit-connection/usb"; const reportMicrobitVersionApiUnsupported = "reportMicrobitVersionApiUnsupported"; @@ -22,7 +22,7 @@ const severityMapping = { export const diagnosticsMapping = ( document: Text, lspDiagnostics: LSP.Diagnostic[], - device: MicrobitWebUSBConnection, + device: MicrobitUSBConnection, warnOnV2OnlyFeatures: boolean, warnOnV2OnlyFeaturesAction: () => Action ): Diagnostic[] => diff --git a/src/editor/codemirror/language-server/view.ts b/src/editor/codemirror/language-server/view.ts index aeec02d33..fcb6ded91 100644 --- a/src/editor/codemirror/language-server/view.ts +++ b/src/editor/codemirror/language-server/view.ts @@ -17,7 +17,7 @@ import { autocompletion } from "./autocompletion"; import { BaseLanguageServerView, clientFacet, uriFacet } from "./common"; import { diagnosticsMapping } from "./diagnostics"; import { signatureHelp } from "./signatureHelp"; -import { MicrobitWebUSBConnection } from "@microbit/microbit-connection"; +import { MicrobitUSBConnection } from "@microbit/microbit-connection/usb"; /** * The main extension. This synchronises the diagnostics between the client @@ -60,7 +60,7 @@ class LanguageServerView extends BaseLanguageServerView implements PluginValue { constructor( view: EditorView, - private device: MicrobitWebUSBConnection, + private device: MicrobitUSBConnection, private intl: IntlShape, private warnOnV2OnlyFeatures: boolean, private disableV2OnlyFeaturesWarning: () => void @@ -126,7 +126,7 @@ interface Actions { */ export function languageServer( client: LanguageServerClient, - device: MicrobitWebUSBConnection, + device: MicrobitUSBConnection, uri: string, intl: IntlShape, logging: Logging, diff --git a/src/fs/fs.test.ts b/src/fs/fs.test.ts index d903d80bb..3081b5746 100644 --- a/src/fs/fs.test.ts +++ b/src/fs/fs.test.ts @@ -8,7 +8,7 @@ import * as fs from "fs"; import * as fsp from "fs/promises"; -import { NullLogging } from "../deployment/default/logging"; +import { ConsoleLogging } from "../deployment/default/logging"; import { MicroPythonSource } from "../micropython/micropython"; import { diff, @@ -45,7 +45,7 @@ const fsMicroPythonSource: MicroPythonSource = async () => { }; describe("Filesystem", () => { - const logging = new NullLogging(); + const logging = new ConsoleLogging(); const host = new DefaultHost(); let ufs = new FileSystem(logging, host, fsMicroPythonSource); let events: Project[] = []; diff --git a/src/fs/fs.ts b/src/fs/fs.ts index ba93f24e5..e2fa57e68 100644 --- a/src/fs/fs.ts +++ b/src/fs/fs.ts @@ -5,16 +5,13 @@ */ import { getIntelHexAppendedScript, + microbitBoardId, MicropythonFsHex, } from "@microbit/microbit-fs"; import { fromByteArray, toByteArray } from "base64-js"; import sortBy from "lodash.sortby"; import { lineNumFromUint8Array } from "../common/text-util"; -import { - BoardId, - FlashDataError, - BoardVersion, -} from "@microbit/microbit-connection"; +import { FlashDataError, BoardVersion } from "@microbit/microbit-connection"; import { Logging } from "../logging/logging"; import { MicroPythonSource } from "../micropython/micropython"; import { extractModuleData, generateId } from "./fs-util"; @@ -464,7 +461,7 @@ export class FileSystem extends TypedEventTarget { return async (boardVersion: BoardVersion) => { try { const fs = await this.initialize(); - const boardId = BoardId.forVersion(boardVersion).id; + const boardId = microbitBoardId[boardVersion]; return fs.getIntelHex(boardId); } catch (e: any) { throw new FlashDataError(e.message); diff --git a/src/fs/storage.test.ts b/src/fs/storage.test.ts index 40c109a95..ee10a7f48 100644 --- a/src/fs/storage.test.ts +++ b/src/fs/storage.test.ts @@ -3,8 +3,8 @@ * * SPDX-License-Identifier: MIT */ +import { ConsoleLogging } from "../deployment/default/logging"; import { MockLogging } from "../logging/mock"; -import { NullLogging } from "../deployment/default/logging"; import { FSStorage, InMemoryFSStorage, @@ -91,7 +91,7 @@ describe("SplitStrategyStorage", () => { const storage = new SplitStrategyStorage( new InMemoryFSStorage(projectName), new SessionStorageFSStorage(sessionStorage), - new NullLogging() + new ConsoleLogging() ); beforeEach(() => { @@ -107,7 +107,11 @@ describe("SplitStrategyStorage", () => { await session.write("test1.py", new Uint8Array([1])); await session.markDirty(); - const split = new SplitStrategyStorage(memory, session, new NullLogging()); + const split = new SplitStrategyStorage( + memory, + session, + new ConsoleLogging() + ); expect(await split.ls()).toEqual(["test1.py"]); expect(await split.projectName()).toEqual("foo"); diff --git a/src/logging/NullLoggingProvider.tsx b/src/logging/NullLoggingProvider.tsx index 8d8ae1c88..d042b1847 100644 --- a/src/logging/NullLoggingProvider.tsx +++ b/src/logging/NullLoggingProvider.tsx @@ -4,11 +4,11 @@ * SPDX-License-Identifier: MIT */ import { ReactNode } from "react"; -import { NullLogging } from "../deployment/default/logging"; +import { ConsoleLogging } from "../deployment/default/logging"; import { LoggingProvider } from "./logging-hooks"; const NullLoggingProvider = ({ children }: { children: ReactNode }) => ( - {children} + {children} ); export default NullLoggingProvider; diff --git a/src/project/SendButton.tsx b/src/project/SendButton.tsx index 68407c014..03ec8a8dd 100644 --- a/src/project/SendButton.tsx +++ b/src/project/SendButton.tsx @@ -40,7 +40,7 @@ const SendButton = React.forwardRef( ref: ForwardedRef ) => { const status = useConnectionStatus(); - const connected = status === ConnectionStatus.CONNECTED; + const connected = status === ConnectionStatus.Connected; const actions = useProjectActions(); const handleToggleConnected = useCallback(async () => { if (connected) { diff --git a/src/project/project-actions.tsx b/src/project/project-actions.tsx index abaf762c3..432010b7e 100644 --- a/src/project/project-actions.tsx +++ b/src/project/project-actions.tsx @@ -19,12 +19,12 @@ import { ActionFeedback } from "../common/use-action-feedback"; import { Dialogs } from "../common/use-dialogs"; import { ConnectionStatus, - MicrobitWebUSBConnection, - AfterRequestDevice, FlashDataError, DeviceError, DeviceErrorCode, + ProgressStage, } from "@microbit/microbit-connection"; +import { MicrobitUSBConnection } from "@microbit/microbit-connection/usb"; import { FileSystem, MAIN_FILE, Statistics, VersionAction } from "../fs/fs"; import { getLowercaseFileExtension, @@ -105,7 +105,7 @@ export enum ConnectionAction { export class ProjectActions { constructor( private fs: FileSystem, - private device: MicrobitWebUSBConnection, + private device: MicrobitUSBConnection, private actionFeedback: ActionFeedback, private dialogs: Dialogs, private setSelection: (selection: WorkbenchSelection) => void, @@ -135,12 +135,14 @@ export class ProjectActions { type: "connect", }); - if (this.device.status === ConnectionStatus.NOT_SUPPORTED) { + const availability = await this.device.checkAvailability(); + if (availability !== "available") { this.webusbNotSupportedError(finalFocusRef); - } else { - if (await this.showConnectHelp(forceConnectHelp, finalFocusRef)) { - return this.connectInternal(userAction, finalFocusRef); - } + return false; + } + + if (await this.showConnectHelp(forceConnectHelp, finalFocusRef)) { + return this.connectInternal(userAction, finalFocusRef); } }; @@ -158,7 +160,7 @@ export class ProjectActions { if ( !force && (!showConnectHelpSetting || - this.device.status === ConnectionStatus.DISCONNECTED) + this.device.status === ConnectionStatus.Disconnected) ) { return true; } @@ -504,14 +506,9 @@ export class ProjectActions { detail: await this.projectStats(), }); - if (this.device.status === ConnectionStatus.NOT_SUPPORTED) { - this.webusbNotSupportedError(finalFocusRef); - return; - } - if ( - this.device.status === ConnectionStatus.NO_AUTHORIZED_DEVICE || - this.device.status === ConnectionStatus.DISCONNECTED + this.device.status === ConnectionStatus.NoAuthorizedDevice || + this.device.status === ConnectionStatus.Disconnected ) { const connected = await this.connect( tryAgain || false, @@ -523,19 +520,23 @@ export class ProjectActions { } } + const flashingCode = this.intl.formatMessage({ id: "flashing-code" }); try { - const flashingCode = this.intl.formatMessage({ id: "flashing-code" }); const firstFlashNotice = ( ); - const progress = (value: number | undefined, partial: boolean) => { - this.dialogs.progress({ - header: flashingCode, - body: partial ? undefined : firstFlashNotice, - progress: value, - }); + const progress = (stage: ProgressStage, value?: number) => { + const isPartial = stage === ProgressStage.PartialFlashing; + const isFlashing = isPartial || stage === ProgressStage.FullFlashing; + if (isFlashing) { + this.dialogs.progress({ + header: flashingCode, + body: isPartial ? undefined : firstFlashNotice, + progress: value, + }); + } }; await this.device.flash(this.fs.asFlashDataSource(), { partial: true, @@ -551,6 +552,8 @@ export class ProjectActions { } else { this.handleWebUSBError(e, ConnectionAction.FLASH, finalFocusRef); } + } finally { + this.dialogs.closeProgress(); } }; @@ -836,11 +839,10 @@ export class ProjectActions { finalFocusRef: FinalFocusRef ) { if (e instanceof DeviceError) { - this.device.dispatchTypedEvent( - "afterrequestdevice", - new AfterRequestDevice() - ); switch (e.code) { + case "unsupported": + this.webusbNotSupportedError(finalFocusRef); + return; case "no-device-selected": { // User just cancelled the browser dialog, perhaps because there // where no devices. diff --git a/src/serial/SerialArea.tsx b/src/serial/SerialArea.tsx index 17617027e..6168fed07 100644 --- a/src/serial/SerialArea.tsx +++ b/src/serial/SerialArea.tsx @@ -42,7 +42,8 @@ const SerialArea = ({ ...props }: SerialAreaProps) => { const status = useConnectionStatus(); - const connected = status === ConnectionStatus.CONNECTED; + const connected = + status === ConnectionStatus.Connected || status === ConnectionStatus.Paused; return ( { + const serialListener = (event: SerialData) => { if (!isUnmounted()) { terminal.write(event.data); } diff --git a/src/serial/serial-actions.ts b/src/serial/serial-actions.ts index da463b8ab..c3b6a0e61 100644 --- a/src/serial/serial-actions.ts +++ b/src/serial/serial-actions.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: MIT */ -import { MicrobitWebUSBConnection } from "@microbit/microbit-connection"; +import { MicrobitUSBConnection } from "@microbit/microbit-connection/usb"; import { Terminal } from "xterm"; import { Logging } from "../logging/logging"; @@ -13,7 +13,7 @@ import { Logging } from "../logging/logging"; export class SerialActions { constructor( private terminal: React.RefObject, - private device: MicrobitWebUSBConnection, + private device: MicrobitUSBConnection, private onSerialSizeChange: (size: "compact" | "open") => void, private logging: Logging ) {} diff --git a/src/simulator/SimulatorSplitView.tsx b/src/simulator/SimulatorSplitView.tsx index e39f8c751..065f1b957 100644 --- a/src/simulator/SimulatorSplitView.tsx +++ b/src/simulator/SimulatorSplitView.tsx @@ -26,7 +26,7 @@ interface SimulatorSplitViewProps { const SimulatorSplitView = ({ simRunning }: SimulatorSplitViewProps) => { const intl = useIntl(); - const connected = useConnectionStatus() === ConnectionStatus.CONNECTED; + const connected = useConnectionStatus() === ConnectionStatus.Connected; const [serialStateWhenOpen, setSerialStateWhenOpen] = useState("compact"); const serialSizedMode = connected ? serialStateWhenOpen : "collapsed"; diff --git a/src/workbench/Workbench.tsx b/src/workbench/Workbench.tsx index 1f74b0fe2..f6fdbfeb8 100644 --- a/src/workbench/Workbench.tsx +++ b/src/workbench/Workbench.tsx @@ -240,7 +240,7 @@ const EditorWithSimulator = ({ const Editor = ({ editor }: EditorProps) => { const intl = useIntl(); - const connected = useConnectionStatus() === ConnectionStatus.CONNECTED; + const connected = useConnectionStatus() === ConnectionStatus.Connected; const [serialStateWhenOpen, setSerialStateWhenOpen] = useState("compact"); const serialSizedMode = connected ? serialStateWhenOpen : "collapsed";