From 92920163d315f189c5a43cebddd1ff8bc5ea01bd Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Tue, 8 Oct 2024 17:38:27 +0100 Subject: [PATCH 1/8] Dialog warnings for using MakeCode with a V1 --- lang/ui.en.json | 24 ++++ src/components/DownloadDialogs.tsx | 10 ++ src/components/IncompatibleEditorDevice.tsx | 122 ++++++++++++++++++++ src/components/TestingModelGridView.tsx | 32 ++++- src/connect-actions.ts | 85 +++++++++----- src/connection-stage-actions.ts | 12 +- src/hooks/download-hooks.tsx | 38 ++++-- src/messages/ui.en.json | 96 ++++++++++----- src/model.ts | 1 + 9 files changed, 342 insertions(+), 78 deletions(-) create mode 100644 src/components/IncompatibleEditorDevice.tsx diff --git a/lang/ui.en.json b/lang/ui.en.json index 433834b2b..d728b8a39 100644 --- a/lang/ui.en.json +++ b/lang/ui.en.json @@ -591,6 +591,10 @@ "defaultMessage": "Training model…", "description": "Progress title" }, + "continue-makecode-action": { + "defaultMessage": "Continue to MakeCode", + "description": "Continue to MakeCode editor button text" + }, "cookies-action": { "defaultMessage": "Cookies", "description": "Action to show dialog to choose website cookie preferences" @@ -751,6 +755,26 @@ "defaultMessage": "Home page", "description": "" }, + "incompatible-device-body-1": { + "defaultMessage": "Continue to MakeCode to edit the program. You can then save the project hex which can be downloaded onto a micro:bit V2.", + "description": "Incompatible device dialog body text" + }, + "incompatible-device-body-2": { + "defaultMessage": "Alternatively, save the project hex without using MakeCode.", + "description": "Incompatible device dialog body text" + }, + "incompatible-device-body-alt": { + "defaultMessage": "Go back to select a different micro:bit, or save the project hex which can be downloaded onto a micro:bit V2 later.", + "description": "Incompatible device dialog body text" + }, + "incompatible-device-heading": { + "defaultMessage": "Incompatible device", + "description": "Incompatible device dialog heading" + }, + "incompatible-device-subtitle": { + "defaultMessage": "This project has code that won't run on a micro:bit V1.", + "description": "Incompatible device dialog subtitle" + }, "insufficient-data-body": { "defaultMessage": "You need at least 3 data samples for 2 actions to train the model.", "description": "Insufficient data modal content" diff --git a/src/components/DownloadDialogs.tsx b/src/components/DownloadDialogs.tsx index 6d6733c5c..2e6693d4a 100644 --- a/src/components/DownloadDialogs.tsx +++ b/src/components/DownloadDialogs.tsx @@ -10,6 +10,7 @@ import { useDownloadActions } from "../hooks/download-hooks"; import { useStore } from "../store"; import UnplugRadioLinkMicrobitDialog from "./UnplugRadioLinkMicrobitDialog"; import ConnectRadioDataCollectionMicrobitDialog from "./ConnectRadioDataCollectionMicrobitDialog"; +import UnsupportedEditorDevice from "./IncompatibleEditorDevice"; const DownloadDialogs = () => { const actions = useDownloadActions(); @@ -98,6 +99,15 @@ const DownloadDialogs = () => { closeIsPrimaryAction={true} /> ); + case DownloadStep.IncompatibleDevice: + return ( + + ); } return <>; }; diff --git a/src/components/IncompatibleEditorDevice.tsx b/src/components/IncompatibleEditorDevice.tsx new file mode 100644 index 000000000..6f3533233 --- /dev/null +++ b/src/components/IncompatibleEditorDevice.tsx @@ -0,0 +1,122 @@ +import { + Button, + HStack, + Heading, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalOverlay, + Text, + VStack, +} from "@chakra-ui/react"; +import { FormattedMessage } from "react-intl"; +import { useProject } from "../hooks/project-hooks"; +import { ReactNode } from "react"; + +interface UnsupportedEditorDeviceProps { + isOpen: boolean; + onClose: () => void; + onNext?: () => void; + onBack?: () => void; + stage: "openEditor" | "flashDevice"; +} + +const UnsupportedEditorDevice = ({ + isOpen, + onClose, + onNext, + onBack, + stage, +}: UnsupportedEditorDeviceProps) => { + const { saveHex } = useProject(); + return ( + + + + + + + + + + + + + + {stage === "openEditor" ? ( + <> + + + + + ( + saveHex()} + > + {chunks} + + ), + }} + /> + + + ) : ( + + ( + saveHex()} + > + {chunks} + + ), + }} + /> + + )} + + + + + + + + + + + + + ); +}; + +export default UnsupportedEditorDevice; diff --git a/src/components/TestingModelGridView.tsx b/src/components/TestingModelGridView.tsx index 5f775c715..2b907bf81 100644 --- a/src/components/TestingModelGridView.tsx +++ b/src/components/TestingModelGridView.tsx @@ -11,9 +11,10 @@ import { Portal, VStack, VisuallyHidden, + useDisclosure, } from "@chakra-ui/react"; import { MakeCodeRenderBlocksProvider } from "@microbit/makecode-embed/react"; -import React from "react"; +import React, { useCallback } from "react"; import { RiArrowRightLine, RiDeleteBin2Line } from "react-icons/ri"; import { FormattedMessage, useIntl } from "react-intl"; import { usePrediction } from "../hooks/ml-hooks"; @@ -29,6 +30,8 @@ import GestureNameGridItem from "./GestureNameGridItem"; import HeadingGrid from "./HeadingGrid"; import LiveGraphPanel from "./LiveGraphPanel"; import MoreMenuButton from "./MoreMenuButton"; +import { useConnectActions } from "../connect-actions-hooks"; +import UnsupportedEditorDevice from "./IncompatibleEditorDevice"; const gridCommonProps: Partial = { gridTemplateColumns: "290px 360px 40px auto", @@ -60,6 +63,7 @@ const TestingModelGridView = () => { const gestures = useStore((s) => s.gestures); const setRequiredConfidence = useStore((s) => s.setRequiredConfidence); const { openEditor, project, resetProject, projectEdited } = useProject(); + const { getBluetoothConnection } = useConnectActions(); const detectedLabel = detected?.name ?? @@ -70,8 +74,32 @@ const TestingModelGridView = () => { const [{ languageId }] = useSettings(); const makeCodeLang = getMakeCodeLang(languageId); + const { isOpen, onOpen, onClose } = useDisclosure(); + + const continueToEditor = useCallback(async () => { + await openEditor(); + onClose(); + }, [onClose, openEditor]); + + const maybeOpenEditor = useCallback(() => { + // Open editor if device is not a V1, otherwise show warning dialog. + const bluetoothConnection = getBluetoothConnection(); + if (bluetoothConnection) { + if (bluetoothConnection.getBoardVersion() === "V1") { + return onOpen(); + } + } + void openEditor(); + }, [getBluetoothConnection, onOpen, openEditor]); + return ( <> + { ), }} /> @@ -80,13 +76,9 @@ const UnsupportedEditorDevice = ({ id="incompatible-device-body-alt" values={{ link: (chunks: ReactNode) => ( - saveHex()} - > + ), }} /> diff --git a/src/components/ManualFlashingDialog.tsx b/src/components/ManualFlashingDialog.tsx index e49a1b2d6..c31e83f3a 100644 --- a/src/components/ManualFlashingDialog.tsx +++ b/src/components/ManualFlashingDialog.tsx @@ -70,9 +70,9 @@ const ManualFlashingDialog = ({ id="connectMB.transferHex.manualDownload" values={{ link: (chunks: ReactNode) => ( - + ), }} /> From 5dc85ea44d2b03748b9b729314d8a38b64e233c4 Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Wed, 9 Oct 2024 11:15:30 +0100 Subject: [PATCH 3/8] Add new method to get data collection board version This should also work for the remote device when using radio bridge. --- src/components/TestingModelGridView.tsx | 11 +++----- src/connect-actions-hooks.tsx | 22 +++++++++++++--- src/connect-actions.ts | 34 ++++++++++++++++++++----- src/connection-stage-actions.ts | 14 +++++++--- src/connection-stage-hooks.tsx | 2 ++ 5 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/components/TestingModelGridView.tsx b/src/components/TestingModelGridView.tsx index 2b907bf81..558138dff 100644 --- a/src/components/TestingModelGridView.tsx +++ b/src/components/TestingModelGridView.tsx @@ -63,7 +63,7 @@ const TestingModelGridView = () => { const gestures = useStore((s) => s.gestures); const setRequiredConfidence = useStore((s) => s.setRequiredConfidence); const { openEditor, project, resetProject, projectEdited } = useProject(); - const { getBluetoothConnection } = useConnectActions(); + const { getDataCollectionBoardVersion } = useConnectActions(); const detectedLabel = detected?.name ?? @@ -83,14 +83,11 @@ const TestingModelGridView = () => { const maybeOpenEditor = useCallback(() => { // Open editor if device is not a V1, otherwise show warning dialog. - const bluetoothConnection = getBluetoothConnection(); - if (bluetoothConnection) { - if (bluetoothConnection.getBoardVersion() === "V1") { - return onOpen(); - } + if (getDataCollectionBoardVersion() === "V1") { + return onOpen(); } void openEditor(); - }, [getBluetoothConnection, onOpen, openEditor]); + }, [getDataCollectionBoardVersion, onOpen, openEditor]); return ( <> diff --git a/src/connect-actions-hooks.tsx b/src/connect-actions-hooks.tsx index dfd616106..8ff5e8176 100644 --- a/src/connect-actions-hooks.tsx +++ b/src/connect-actions-hooks.tsx @@ -1,4 +1,5 @@ import { + BoardVersion, MicrobitRadioBridgeConnection, MicrobitWebBluetoothConnection, MicrobitWebUSBConnection, @@ -19,6 +20,7 @@ interface ConnectContextValue { usb: MicrobitWebUSBConnection; bluetooth: MicrobitWebBluetoothConnection; radioBridge: MicrobitRadioBridgeConnection; + radioRemoteBoardVersion: React.MutableRefObject; } const ConnectContext = createContext(null); @@ -50,8 +52,12 @@ export const ConnectProvider = ({ children }: ConnectProviderProps) => { } }, [bluetooth, isInitialized, radioBridge, usb]); + const radioRemoteBoardVersion = useRef(); + return ( - + {isInitialized ? children : <>} ); @@ -62,12 +68,20 @@ export const useConnectActions = (): ConnectActions => { if (!connectContextValue) { throw new Error("Missing provider"); } - const { usb, bluetooth, radioBridge } = connectContextValue; + const { usb, bluetooth, radioBridge, radioRemoteBoardVersion } = + connectContextValue; const logging = useLogging(); const connectActions = useMemo( - () => new ConnectActions(logging, usb, bluetooth, radioBridge), - [bluetooth, logging, radioBridge, usb] + () => + new ConnectActions( + logging, + usb, + bluetooth, + radioBridge, + radioRemoteBoardVersion + ), + [bluetooth, logging, radioBridge, radioRemoteBoardVersion, usb] ); return connectActions; diff --git a/src/connect-actions.ts b/src/connect-actions.ts index 5f72325b2..7ce5915a2 100644 --- a/src/connect-actions.ts +++ b/src/connect-actions.ts @@ -1,5 +1,6 @@ import { AccelerometerDataEvent, + BoardVersion, ButtonEvent, ConnectionStatus, ConnectionStatusEvent, @@ -58,7 +59,10 @@ export class ConnectActions { private logging: Logging, private usb: MicrobitWebUSBConnection, private bluetooth: MicrobitWebBluetoothConnection, - private radioBridge: MicrobitRadioBridgeConnection + private radioBridge: MicrobitRadioBridgeConnection, + private radioRemoteBoardVersion: React.MutableRefObject< + BoardVersion | undefined + > ) { this.isWebBluetoothSupported = bluetooth.status !== DeviceConnectionStatus.NOT_SUPPORTED; @@ -137,16 +141,24 @@ export class ConnectActions { hex: string | HexType, progressCallback: (progress: number) => void ): Promise< - | { result: ConnectResult.Success; deviceId: number } - | { result: ConnectAndFlashFailResult; deviceId?: number } + | { + result: ConnectResult.Success; + deviceId: number; + boardVersion?: BoardVersion; + } + | { + result: ConnectAndFlashFailResult; + deviceId?: number; + boardVersion?: BoardVersion; + } > => { - const { result, deviceId } = await this.requestUSBConnection(); + const { result, deviceId, usb } = await this.requestUSBConnection(); if (result !== ConnectResult.Success) { return { result }; } try { const result = await this.flashMicrobit(hex, progressCallback); - return { result, deviceId }; + return { result, deviceId, boardVersion: usb.getBoardVersion() }; } catch (e) { this.logging.error( `USB request device failed/cancelled: ${JSON.stringify(e)}` @@ -173,7 +185,11 @@ export class ConnectActions { return ConnectResult.Failed; }; - connectMicrobitsSerial = async (deviceId: number): Promise => { + connectMicrobitsSerial = async ( + deviceId: number, + boardVersion?: BoardVersion + ): Promise => { + this.radioRemoteBoardVersion.current = boardVersion; this.radioBridge.setRemoteDeviceId(deviceId); await this.radioBridge.connect(); }; @@ -198,6 +214,12 @@ export class ConnectActions { return this.usb.getDevice(); }; + getDataCollectionBoardVersion = (): BoardVersion | undefined => { + return ( + this.bluetooth.getBoardVersion() ?? this.radioRemoteBoardVersion.current + ); + }; + clearUsbDevice = async () => { await this.usb.clearDevice(); }; diff --git a/src/connection-stage-actions.ts b/src/connection-stage-actions.ts index b9c7fcb96..789713f6e 100644 --- a/src/connection-stage-actions.ts +++ b/src/connection-stage-actions.ts @@ -1,3 +1,4 @@ +import { BoardVersion } from "@microbit/microbit-connection"; import { deviceIdToMicrobitName } from "./bt-pattern-utils"; import { ConnectActions, @@ -65,13 +66,13 @@ export class ConnectionStageActions { ) => { this.setFlowStep(ConnectionFlowStep.WebUsbChooseMicrobit); const hex = this.getHexType(); - const { result, deviceId } = + const { result, deviceId, boardVersion } = await this.actions.requestUSBConnectionAndFlash(hex, progressCallback); if (result !== ConnectResult.Success) { return this.handleConnectAndFlashFail(result); } - await this.onFlashSuccess(deviceId, onSuccess); + await this.onFlashSuccess(deviceId, onSuccess, boardVersion); }; private getHexType = () => { @@ -84,7 +85,8 @@ export class ConnectionStageActions { private onFlashSuccess = async ( deviceId: number, - onSuccess: (stage: ConnectionStage) => void + onSuccess: (stage: ConnectionStage) => void, + boardVersion?: BoardVersion ) => { let newStage = this.stage; // Store radio/bluetooth details. Radio is essential to pass to micro:bit 2. @@ -113,6 +115,7 @@ export class ConnectionStageActions { connType: "radio", flowStep: ConnectionFlowStep.ConnectBattery, radioRemoteDeviceId: deviceId, + radioRemoteBoardVersion: boardVersion, }; break; } @@ -176,7 +179,10 @@ export class ConnectionStageActions { if (!newStage.radioRemoteDeviceId) { throw new Error("Radio bridge device id not set"); } - await this.actions.connectMicrobitsSerial(newStage.radioRemoteDeviceId); + await this.actions.connectMicrobitsSerial( + newStage.radioRemoteDeviceId, + newStage.radioRemoteBoardVersion + ); }; private getConnectingStage = (connType: ConnectionType) => { diff --git a/src/connection-stage-hooks.tsx b/src/connection-stage-hooks.tsx index e681cc576..1ce3394ba 100644 --- a/src/connection-stage-hooks.tsx +++ b/src/connection-stage-hooks.tsx @@ -16,6 +16,7 @@ import { import { ConnectionStageActions } from "./connection-stage-actions"; import { useStorage } from "./hooks/use-storage"; import { useStore } from "./store"; +import { BoardVersion } from "@microbit/microbit-connection"; export enum ConnectionFlowType { ConnectBluetooth = "ConnectBluetooth", @@ -77,6 +78,7 @@ export interface ConnectionStage { bluetoothMicrobitName?: string; radioBridgeDeviceId?: number; radioRemoteDeviceId?: number; + radioRemoteBoardVersion?: BoardVersion; hasFailedToReconnectTwice: boolean; // User Project From 728ab8f766c177c6ce7dd2ba68b8061d164ea810 Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Wed, 9 Oct 2024 11:49:42 +0100 Subject: [PATCH 4/8] Remove added method that is now unused --- src/connect-actions.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/connect-actions.ts b/src/connect-actions.ts index 7ce5915a2..a99361e50 100644 --- a/src/connect-actions.ts +++ b/src/connect-actions.ts @@ -206,15 +206,12 @@ export class ConnectActions { return this.usb; }; - getBluetoothConnection = () => { - return this.bluetooth; - }; - getUsbDevice = () => { return this.usb.getDevice(); }; getDataCollectionBoardVersion = (): BoardVersion | undefined => { + console.log(this.radioRemoteBoardVersion.current); return ( this.bluetooth.getBoardVersion() ?? this.radioRemoteBoardVersion.current ); From ba6c740a58bf0402f1c5e1d5d8887bb08ed88511 Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Wed, 9 Oct 2024 11:49:59 +0100 Subject: [PATCH 5/8] This change is no longer required, revert --- src/hooks/download-hooks.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/hooks/download-hooks.tsx b/src/hooks/download-hooks.tsx index 1e2b5060f..4f3da0ebd 100644 --- a/src/hooks/download-hooks.tsx +++ b/src/hooks/download-hooks.tsx @@ -34,8 +34,6 @@ export class DownloadProjectActions { private connectionStatus: ConnectionStatus ) {} - private connectionAndFlashOptions: ConnectionAndFlashOptions | undefined; - clearMakeCodeUsbDevice = () => { this.setState({ ...this.state, usbDevice: undefined }); }; @@ -127,7 +125,7 @@ export class DownloadProjectActions { }; connectAndFlashMicrobit = async (stage: DownloadState) => { - this.connectionAndFlashOptions = undefined; + let connectionAndFlashOptions: ConnectionAndFlashOptions | undefined; if ( stage.microbitToFlash === MicrobitToFlash.Same && this.connectionStage.connType === "bluetooth" @@ -146,7 +144,7 @@ export class DownloadProjectActions { { serialNumber: connectedDevice.serialNumber }, ]); } - this.connectionAndFlashOptions = { + connectionAndFlashOptions = { temporaryUsbConnection, callbackIfDeviceIsSame: this.connectionStageActions.disconnectInputMicrobit, @@ -159,7 +157,7 @@ export class DownloadProjectActions { this.updateStage({ step: DownloadStep.WebUsbChooseMicrobit }); const { result, usb } = await this.connectActions.requestUSBConnection( - this.connectionAndFlashOptions + connectionAndFlashOptions ); if (result === ConnectResult.Success && usb.getBoardVersion() === "V1") { return this.updateStage({ @@ -167,7 +165,7 @@ export class DownloadProjectActions { }); } - await this.flashMicrobit(stage, this.connectionAndFlashOptions); + await this.flashMicrobit(stage, connectionAndFlashOptions); }; flashMicrobit = async ( From 0d65f463960597036172fda5483131735240d34a Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Wed, 9 Oct 2024 20:36:37 +0100 Subject: [PATCH 6/8] Update text --- lang/ui.en.json | 2 +- src/components/IncompatibleEditorDevice.tsx | 12 +++++++++++- src/messages/ui.en.json | 12 +++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/lang/ui.en.json b/lang/ui.en.json index d728b8a39..f7e165265 100644 --- a/lang/ui.en.json +++ b/lang/ui.en.json @@ -772,7 +772,7 @@ "description": "Incompatible device dialog heading" }, "incompatible-device-subtitle": { - "defaultMessage": "This project has code that won't run on a micro:bit V1.", + "defaultMessage": "You are using a micro:bit V1 but machine learning projects need the faster processor on a micro:bit V2. Learn more about micro:bit versions.", "description": "Incompatible device dialog subtitle" }, "insufficient-data-body": { diff --git a/src/components/IncompatibleEditorDevice.tsx b/src/components/IncompatibleEditorDevice.tsx index 99c838be3..0c3f4aba4 100644 --- a/src/components/IncompatibleEditorDevice.tsx +++ b/src/components/IncompatibleEditorDevice.tsx @@ -2,6 +2,7 @@ import { Button, HStack, Heading, + Link, Modal, ModalBody, ModalCloseButton, @@ -50,7 +51,16 @@ const UnsupportedEditorDevice = ({ - + ( + + {children} + + ), + }} + /> {stage === "openEditor" ? ( <> diff --git a/src/messages/ui.en.json b/src/messages/ui.en.json index 93d9f48de..2e8af4c58 100644 --- a/src/messages/ui.en.json +++ b/src/messages/ui.en.json @@ -1392,7 +1392,17 @@ "incompatible-device-subtitle": [ { "type": 0, - "value": "This project has code that won't run on a micro:bit V1." + "value": "You are using a micro:bit V1 but machine learning projects need the faster processor on a micro:bit V2. " + }, + { + "children": [ + { + "type": 0, + "value": "Learn more about micro:bit versions." + } + ], + "type": 8, + "value": "link" } ], "insufficient-data-body": [ From fdeef8db6b56a8c3bcf8d7b1924611d71a1ed4a2 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Wed, 9 Oct 2024 20:46:03 +0100 Subject: [PATCH 7/8] Update dialog --- src/components/IncompatibleEditorDevice.tsx | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/IncompatibleEditorDevice.tsx b/src/components/IncompatibleEditorDevice.tsx index 0c3f4aba4..abf401524 100644 --- a/src/components/IncompatibleEditorDevice.tsx +++ b/src/components/IncompatibleEditorDevice.tsx @@ -1,20 +1,20 @@ import { Button, HStack, - Heading, Link, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, + ModalHeader, ModalOverlay, Text, VStack, } from "@chakra-ui/react"; +import { ReactNode } from "react"; import { FormattedMessage } from "react-intl"; import { useProject } from "../hooks/project-hooks"; -import { ReactNode } from "react"; interface UnsupportedEditorDeviceProps { isOpen: boolean; @@ -42,20 +42,23 @@ const UnsupportedEditorDevice = ({ isCentered > - + + + + + - - - - ( - + {children} ), @@ -97,7 +100,7 @@ const UnsupportedEditorDevice = ({ - +