diff --git a/package-lock.json b/package-lock.json index 65fbb900d..00f6cb1b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@microbit/microbit-connection": "^0.0.0-alpha.13", + "@microbit/microbit-connection": "^0.0.0-alpha.14", "@tensorflow/tfjs": "^4.4.0", "@types/w3c-web-serial": "^1.0.6", "@types/w3c-web-usb": "^1.0.6", @@ -4356,9 +4356,9 @@ "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" }, "node_modules/@microbit/microbit-connection": { - "version": "0.0.0-alpha.13", - "resolved": "https://registry.npmjs.org/@microbit/microbit-connection/-/microbit-connection-0.0.0-alpha.13.tgz", - "integrity": "sha512-TwlKERm+WmcA4wFK5ioV5S/ah71SEH4wFqNdrfgyMO5U1QZfmG+H3DEMWBuYiI67NLn4ZED71vadgSMzwuOaxg==", + "version": "0.0.0-alpha.14", + "resolved": "https://registry.npmjs.org/@microbit/microbit-connection/-/microbit-connection-0.0.0-alpha.14.tgz", + "integrity": "sha512-Z6PsxYTO359KD5hah0JOlxxehhlzmd7Q45NcrDYT6njTjbAT9FbSF4KnnQbgnbRgePmBzW4q/HAUl89Nvqevdw==", "dependencies": { "@microbit/microbit-universal-hex": "^0.2.2", "@types/web-bluetooth": "^0.0.20", diff --git a/package.json b/package.json index 7d0e5ab91..c3107931d 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@microbit/microbit-connection": "^0.0.0-alpha.13", + "@microbit/microbit-connection": "^0.0.0-alpha.14", "@tensorflow/tfjs": "^4.4.0", "@types/w3c-web-serial": "^1.0.6", "@types/w3c-web-usb": "^1.0.6", diff --git a/src/App.tsx b/src/App.tsx index 2efcfe26a..0570011b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,6 +25,7 @@ import { GesturesProvider } from "./gestures-hooks"; import { MlStatusProvider } from "./ml-status-hooks"; import { ConnectionStageProvider } from "./connection-stage-hooks"; import { ConnectProvider } from "./connect-actions-hooks"; +import { ConnectStatusProvider } from "./connect-status-hooks"; export interface ProviderLayoutProps { children: ReactNode; @@ -43,13 +44,15 @@ const Providers = ({ children }: ProviderLayoutProps) => { - - - - {children} - - - + + + + + {children} + + + + diff --git a/src/components/ConnectCableDialog.tsx b/src/components/ConnectCableDialog.tsx index 5e17e1d84..e641f5846 100644 --- a/src/components/ConnectCableDialog.tsx +++ b/src/components/ConnectCableDialog.tsx @@ -1,4 +1,4 @@ -import { Image, Text, VStack } from "@chakra-ui/react"; +import { Button, Image, Text, VStack } from "@chakra-ui/react"; import { FormattedMessage } from "react-intl"; import connectCableImage from "../images/connect-cable.gif"; import ConnectContainerDialog, { @@ -51,7 +51,7 @@ const ConnectCableDialog = ({ onSwitch, ...props }: ConnectCableDialogProps) => { - const { subtitleId, onLink, ...typeProps } = configs[type]; + const { subtitleId, onLink, linkTextId, headingId } = configs[type]; const linkConfig = { [LinkType.None]: undefined, [LinkType.Skip]: onSkip, @@ -59,9 +59,15 @@ const ConnectCableDialog = ({ }; return ( + + + ) + } > diff --git a/src/components/ConnectContainerDialog.tsx b/src/components/ConnectContainerDialog.tsx index f0add80ce..9b4148564 100644 --- a/src/components/ConnectContainerDialog.tsx +++ b/src/components/ConnectContainerDialog.tsx @@ -19,8 +19,7 @@ export interface ConnectContainerDialogProps { isOpen: boolean; onClose: () => void; headingId: string; - onLinkClick?: () => void; - linkTextId?: string; + footerLeft?: ReactNode; onNextClick?: () => void; children: ReactNode; onBackClick?: () => void; @@ -30,8 +29,7 @@ const ConnectContainerDialog = ({ isOpen, onClose, headingId, - onLinkClick, - linkTextId, + footerLeft, onNextClick, onBackClick, children, @@ -57,14 +55,10 @@ const ConnectContainerDialog = ({ - {onLinkClick && linkTextId && ( - - )} + {footerLeft && footerLeft} {onBackClick && ( @@ -58,15 +56,15 @@ const LiveGraphPanel = () => { variant="primary" size="sm" isDisabled={ - stage.status === ConnectionStatus.Reconnecting || - stage.status === ConnectionStatus.Connecting + status === ConnectionStatus.Reconnecting || + status === ConnectionStatus.Connecting } onClick={connectBtnConfig.onClick} > )} - {stage.status === ConnectionStatus.Reconnecting && ( + {status === ConnectionStatus.Reconnecting && ( diff --git a/src/components/ReconnectErrorDialog.tsx b/src/components/ReconnectErrorDialog.tsx index 76567c50b..717aa4b83 100644 --- a/src/components/ReconnectErrorDialog.tsx +++ b/src/components/ReconnectErrorDialog.tsx @@ -26,8 +26,8 @@ interface ReconnectErrorDialogProps { onReconnect: () => void; flowType: ConnectionFlowType; errorStep: - | ConnectionFlowStep.ReconnectManualFail - | ConnectionFlowStep.ReconnectAutoFail; + | ConnectionFlowStep.ReconnectFailed + | ConnectionFlowStep.ConnectionLost; } const contentConfig = { @@ -55,8 +55,8 @@ const contentConfig = { }; const errorTextIdPrefixConfig = { - [ConnectionFlowStep.ReconnectAutoFail]: "disconnectedWarning", - [ConnectionFlowStep.ReconnectManualFail]: "reconnectFailed", + [ConnectionFlowStep.ConnectionLost]: "disconnectedWarning", + [ConnectionFlowStep.ReconnectFailed]: "reconnectFailed", }; const ReconnectErrorDialog = ({ diff --git a/src/components/WhatYouWillNeedDialog.tsx b/src/components/WhatYouWillNeedDialog.tsx index 0098db768..efc6455fe 100644 --- a/src/components/WhatYouWillNeedDialog.tsx +++ b/src/components/WhatYouWillNeedDialog.tsx @@ -1,4 +1,4 @@ -import { Grid, GridItem, Image, Text, VStack } from "@chakra-ui/react"; +import { Button, Grid, GridItem, Image, Text, VStack } from "@chakra-ui/react"; import { FormattedMessage } from "react-intl"; import batteryPackImage from "../images/stylised-battery-pack.svg"; import microbitImage from "../images/stylised-microbit-black.svg"; @@ -9,6 +9,7 @@ import computerBluetoothImage from "../images/stylised_computer_w_bluetooth.svg" import ConnectContainerDialog, { ConnectContainerDialogProps, } from "./ConnectContainerDialog"; +import ExternalLink from "./ExternalLink"; const itemsConfig = { radio: [ @@ -60,24 +61,42 @@ export interface WhatYouWillNeedDialogProps > { reconnect: boolean; type: "radio" | "bluetooth"; + onLinkClick: (() => void) | undefined; } const WhatYouWillNeedDialog = ({ reconnect, type, + onLinkClick, ...props }: WhatYouWillNeedDialogProps) => { return ( + {onLinkClick && ( + + )} + {reconnect && ( + + )} + + } > {reconnect && ( diff --git a/src/connect-actions.ts b/src/connect-actions.ts index 9db6a9de0..6516ed0b0 100644 --- a/src/connect-actions.ts +++ b/src/connect-actions.ts @@ -1,5 +1,6 @@ import { AccelerometerDataEvent, + ConnectionStatusEvent, ButtonEvent, ConnectionStatus as DeviceConnectionStatus, DeviceError, @@ -78,7 +79,7 @@ export class ConnectActions { try { await this.usb.flash(data, { partial: true, - progress: (v) => progress(v ?? 100), + progress: (v: number | undefined) => progress(v ?? 100), }); return ConnectAndFlashResult.Success; } catch (e) { @@ -118,16 +119,16 @@ export class ConnectActions { }; connectBluetooth = async ( - name: string | undefined - ): Promise => { + name: string | undefined, + clearDevice: boolean + ): Promise => { + if (clearDevice) { + await this.bluetooth.clearDevice(); + } if (name) { this.bluetooth.setNameFilter(name); } await this.bluetooth.connect(); - if (this.bluetooth.status === DeviceConnectionStatus.CONNECTED) { - return ConnectResult.Success; - } - return ConnectResult.ManualConnectFailed; }; addAccelerometerListener = ( @@ -166,4 +167,12 @@ export class ConnectActions { await this.bluetooth.disconnect(); await this.radioBridge.disconnect(); }; + + addStatusListener = (listener: (e: ConnectionStatusEvent) => void) => { + this.bluetooth.addEventListener("status", listener); + }; + + removeStatusListener = (listener: (e: ConnectionStatusEvent) => void) => { + this.bluetooth.removeEventListener("status", listener); + }; } diff --git a/src/connect-status-hooks.tsx b/src/connect-status-hooks.tsx new file mode 100644 index 000000000..4e7c7864c --- /dev/null +++ b/src/connect-status-hooks.tsx @@ -0,0 +1,189 @@ +import { + ConnectionStatusEvent, + ConnectionStatus as DeviceConnectionStatus, +} from "@microbit/microbit-connection"; +import { + MutableRefObject, + ReactNode, + createContext, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { useConnectActions } from "./connect-actions-hooks"; + +export enum ConnectionStatus { + /** + * Represents the initial connection status. + */ + NotConnected = "NotConnected", + /** + * Connecting occurs for the initial connection. + */ + Connecting = "Connecting", + /** + * Connected. + */ + Connected = "Connected", + /** + * Reconnecting occurs for the subsequent connections after the initial one. + */ + Reconnecting = "Reconnecting", + /** + * Disconnected. The disconnection is triggered by the user. + */ + Disconnected = "Disconnected", + /** + * Failure to establish initial connection triggered by the user. + */ + FailedToConnect = "FailedToConnect", + /** + * Failure to reconnect triggered by the user. + */ + FailedToReconnect = "FailedToReconnect", + /** + * Connection lost. Auto-reconnect was attempted, but failed. + */ + ConnectionLost = "ConnectionLost", + /** + * A subsequent failure to reconnect after a reconnection failure. + * The initial reconnection failure may have been triggered automatically + * or by the user (ConnectionLost or FailedToReconnect). + */ + FailedToReconnectTwice = "FailedToReconnectTwice", +} + +type ConnectStatusContextValue = [ + ConnectionStatus, + (status: ConnectionStatus) => void +]; + +const ConnectStatusContext = createContext( + null +); + +interface ConnectStatusProviderProps { + children: ReactNode; +} + +export const ConnectStatusProvider = ({ + children, +}: ConnectStatusProviderProps) => { + const connectStatusContextValue = useState( + ConnectionStatus.NotConnected + ); + return ( + + {children} + + ); +}; + +export const useSetConnectStatus = (): ((status: ConnectionStatus) => void) => { + const connectStatusContextValue = useContext(ConnectStatusContext); + if (!connectStatusContextValue) { + throw new Error("Missing provider"); + } + const [, setStatus] = connectStatusContextValue; + + return setStatus; +}; + +export const useConnectStatus = ( + handleStatus?: (status: ConnectionStatus) => void +): ConnectionStatus => { + const connectStatusContextValue = useContext(ConnectStatusContext); + if (!connectStatusContextValue) { + throw new Error("Missing provider"); + } + const [status, setStatus] = connectStatusContextValue; + const connectActions = useConnectActions(); + const prevDeviceStatus = useRef(null); + const hasAttempedReconnect = useRef(false); + + useEffect(() => { + const listener = ({ status: deviceStatus }: ConnectionStatusEvent) => { + const newStatus = getNextConnectionStatus( + status, + deviceStatus, + prevDeviceStatus.current, + hasAttempedReconnect + ); + prevDeviceStatus.current = deviceStatus; + if (newStatus) { + handleStatus && handleStatus(newStatus); + setStatus(newStatus); + } + }; + connectActions.addStatusListener(listener); + return () => { + connectActions.removeStatusListener(listener); + }; + }, [connectActions, handleStatus, setStatus, status]); + + return status; +}; + +const getNextConnectionStatus = ( + status: ConnectionStatus, + deviceStatus: DeviceConnectionStatus, + prevDeviceStatus: DeviceConnectionStatus | null, + hasAttempedReconnect: MutableRefObject +) => { + if ( + // Disconnection happens for newly started / restarted + // connection flows when clearing device + deviceStatus === DeviceConnectionStatus.DISCONNECTED && + status === ConnectionStatus.NotConnected + ) { + return ConnectionStatus.NotConnected; + } + if (deviceStatus === DeviceConnectionStatus.CONNECTED) { + hasAttempedReconnect.current = false; + return ConnectionStatus.Connected; + } + if ( + (status === ConnectionStatus.Connecting && + deviceStatus === DeviceConnectionStatus.DISCONNECTED) || + // If user does not select a device + (deviceStatus === DeviceConnectionStatus.NO_AUTHORIZED_DEVICE && + prevDeviceStatus === DeviceConnectionStatus.NO_AUTHORIZED_DEVICE) + ) { + return ConnectionStatus.FailedToConnect; + } + if ( + hasAttempedReconnect.current && + deviceStatus === DeviceConnectionStatus.DISCONNECTED && + prevDeviceStatus === DeviceConnectionStatus.CONNECTING + ) { + return ConnectionStatus.FailedToReconnectTwice; + } + if ( + deviceStatus === DeviceConnectionStatus.DISCONNECTED && + prevDeviceStatus === DeviceConnectionStatus.CONNECTING + ) { + hasAttempedReconnect.current = true; + return ConnectionStatus.FailedToReconnect; + } + if ( + deviceStatus === DeviceConnectionStatus.DISCONNECTED && + prevDeviceStatus === DeviceConnectionStatus.RECONNECTING + ) { + hasAttempedReconnect.current = true; + return ConnectionStatus.ConnectionLost; + } + if (deviceStatus === DeviceConnectionStatus.DISCONNECTED) { + return ConnectionStatus.Disconnected; + } + if ( + deviceStatus === DeviceConnectionStatus.RECONNECTING || + deviceStatus === DeviceConnectionStatus.CONNECTING + ) { + return status === ConnectionStatus.NotConnected || + status === ConnectionStatus.FailedToConnect + ? ConnectionStatus.Connecting + : ConnectionStatus.Reconnecting; + } + return undefined; +}; diff --git a/src/connection-stage-actions.ts b/src/connection-stage-actions.ts index 17beb78ba..e7a59fc7c 100644 --- a/src/connection-stage-actions.ts +++ b/src/connection-stage-actions.ts @@ -10,9 +10,10 @@ import { ConnectionFlowStep, ConnectionFlowType, ConnectionStage, - ConnectionStatus, + ConnectionType, } from "./connection-stage-hooks"; import { createStepPageUrl } from "./urls"; +import { ConnectionStatus } from "./connect-status-hooks"; type FlowStage = Pick; @@ -21,12 +22,15 @@ export class ConnectionStageActions { private actions: ConnectActions, private navigate: NavigateFunction, private stage: ConnectionStage, - private setStage: (stage: ConnectionStage) => void + private setStage: (stage: ConnectionStage) => void, + private setStatus: (status: ConnectionStatus) => void ) {} - start = () => + start = () => { + this.setStatus(ConnectionStatus.NotConnected); this.setStage({ ...this.stage, + hasFailedToReconnectTwice: false, flowType: this.stage.flowType === ConnectionFlowType.RadioBridge ? ConnectionFlowType.RadioRemote @@ -36,6 +40,7 @@ export class ConnectionStageActions { ? ConnectionFlowStep.WebUsbBluetoothUnsupported : ConnectionFlowStep.Start, }); + }; setFlowStep = (step: ConnectionFlowStep) => { this.setStage({ ...this.stage, flowStep: step }); @@ -79,11 +84,8 @@ export class ConnectionStageActions { } case ConnectionFlowType.RadioBridge: { newStage = { - ...this.stage, - connType: "radio", - flowStep: ConnectionFlowStep.ConnectingMicrobits, + ...this.getConnectingStage("radio"), radioBridgeDeviceId: deviceId, - status: this.getConnectingOrReconnectingStatus(), }; break; } @@ -136,17 +138,12 @@ export class ConnectionStageActions { }); }; - connectBluetooth = async () => { - this.setStage({ - ...this.stage, - connType: "bluetooth", - flowStep: ConnectionFlowStep.ConnectingBluetooth, - status: this.getConnectingOrReconnectingStatus(), - }); - const result = await this.actions.connectBluetooth( - this.stage.bluetoothMicrobitName + connectBluetooth = async (clearDevice: boolean = true) => { + this.setStage(this.getConnectingStage("bluetooth")); + await this.actions.connectBluetooth( + this.stage.bluetoothMicrobitName, + clearDevice ); - this.handleConnectResult(result); }; connectMicrobits = async () => { @@ -160,76 +157,79 @@ export class ConnectionStageActions { } }; - private getConnectingOrReconnectingStatus = () => { - return this.stage.status === ConnectionStatus.None - ? ConnectionStatus.Connecting - : ConnectionStatus.Reconnecting; + private getConnectingStage = (connType: ConnectionType) => { + return { + ...this.stage, + connType, + flowStep: + connType === "bluetooth" + ? ConnectionFlowStep.ConnectingBluetooth + : ConnectionFlowStep.ConnectingMicrobits, + }; }; private handleConnectResult = (result: ConnectResult) => { if (result === ConnectResult.Success) { + // TODO: Remove forced set status and listen to status event + // from connection library instead for radio + if (this.stage.connType === "radio") { + this.setStatus(ConnectionStatus.Connected); + } return this.onConnected(); } - const newReconnectFailStreak = - this.stage.status === ConnectionStatus.Reconnecting - ? this.stage.reconnectFailStreak + 1 - : this.stage.reconnectFailStreak; - - const nextFlowStep = this.getReconnectFailFlowStep( - newReconnectFailStreak, - result - ); - this.setStage({ - ...this.stage, - reconnectFailStreak: newReconnectFailStreak, - status: ConnectionStatus.Disconnected, - flowStep: nextFlowStep, - }); + this.handleConnectFail(); }; - private getReconnectFailFlowStep = ( - failStreak: number, - result: ConnectResult - ) => { - switch (failStreak) { - case 0: { - return this.stage.flowType === ConnectionFlowType.Bluetooth - ? ConnectionFlowStep.TryAgainBluetoothConnect - : ConnectionFlowStep.TryAgainReplugMicrobit; - } - case 1: { - return result === ConnectResult.ManualConnectFailed - ? ConnectionFlowStep.ReconnectManualFail - : ConnectionFlowStep.ReconnectAutoFail; - } - default: { - return ConnectionFlowStep.ReconnectFailedTwice; - } - } + private handleConnectFail = () => { + this.setFlowStep( + this.stage.flowType === ConnectionFlowType.Bluetooth + ? ConnectionFlowStep.TryAgainBluetoothConnect + : ConnectionFlowStep.TryAgainReplugMicrobit + ); }; private onConnected = () => { - this.setStage({ - ...this.stage, - flowStep: ConnectionFlowStep.None, - status: ConnectionStatus.Connected, - reconnectFailStreak: 0, - }); + this.setFlowStep(ConnectionFlowStep.None); this.navigate(createStepPageUrl("add-data")); }; disconnect = async () => { await this.actions.disconnect(); - this.setStage({ - ...this.stage, - status: ConnectionStatus.Disconnected, - }); + }; + + handleConnectionStatus = (status: ConnectionStatus) => { + switch (status) { + case ConnectionStatus.Connected: { + return this.onConnected(); + } + case ConnectionStatus.FailedToConnect: { + return this.handleConnectFail(); + } + case ConnectionStatus.FailedToReconnectTwice: { + return this.setStage({ + ...this.stage, + hasFailedToReconnectTwice: true, + flowStep: ConnectionFlowStep.ReconnectFailedTwice, + }); + } + case ConnectionStatus.FailedToReconnect: { + return this.setFlowStep(ConnectionFlowStep.ReconnectFailed); + } + case ConnectionStatus.ConnectionLost: { + return this.setFlowStep(ConnectionFlowStep.ConnectionLost); + } + case ConnectionStatus.Reconnecting: { + return this.setStage(this.getConnectingStage("bluetooth")); + } + } + return; }; reconnect = async () => { if (this.stage.connType === "bluetooth") { - await this.connectBluetooth(); + await this.connectBluetooth(false); } else { + this.setStage(this.getConnectingStage("radio")); await this.connectMicrobits(); } }; @@ -252,12 +252,30 @@ export class ConnectionStageActions { }); }; + private getStagesOrder = () => { + if (this.stage.flowType === ConnectionFlowType.Bluetooth) { + return bluetoothFlow({ + isManualFlashing: + !this.stage.isWebUsbSupported || + this.stage.flowStep === ConnectionFlowStep.ManualFlashingTutorial, + isRestartAgain: this.stage.hasFailedToReconnectTwice, + }); + } + return radioFlow(); + }; + onNextClick = () => { - this.setStage({ ...this.stage, ...getNextStage(this.stage, 1) }); + this.setStage({ + ...this.stage, + ...getNextStage(this.stage, 1, this.getStagesOrder()), + }); }; onBackClick = () => { - this.setStage({ ...this.stage, ...getNextStage(this.stage, -1) }); + this.setStage({ + ...this.stage, + ...getNextStage(this.stage, -1, this.getStagesOrder()), + }); }; onTryAgain = () => { @@ -269,47 +287,70 @@ export class ConnectionStageActions { }; } -const getStagesOrder = (state: ConnectionStage): FlowStage[] => { - const { RadioRemote, RadioBridge, Bluetooth } = ConnectionFlowType; - if (state.flowType === ConnectionFlowType.Bluetooth) { - return [ - { flowStep: ConnectionFlowStep.Start, flowType: Bluetooth }, - { flowStep: ConnectionFlowStep.ConnectCable, flowType: Bluetooth }, - // Only bluetooth mode has this fallback, the radio bridge mode requires working WebUSB. - { - flowStep: - !state.isWebUsbSupported || - state.flowStep === ConnectionFlowStep.ManualFlashingTutorial - ? ConnectionFlowStep.ManualFlashingTutorial - : ConnectionFlowStep.WebUsbFlashingTutorial, - flowType: Bluetooth, - }, - { flowStep: ConnectionFlowStep.ConnectBattery, flowType: Bluetooth }, - { - flowStep: ConnectionFlowStep.EnterBluetoothPattern, - flowType: Bluetooth, - }, - { - flowStep: ConnectionFlowStep.ConnectBluetoothTutorial, - flowType: Bluetooth, - }, - ]; - } - return [ - { flowStep: ConnectionFlowStep.Start, flowType: RadioRemote }, - { flowStep: ConnectionFlowStep.ConnectCable, flowType: RadioRemote }, - { - flowStep: ConnectionFlowStep.WebUsbFlashingTutorial, - flowType: RadioRemote, - }, - { flowStep: ConnectionFlowStep.ConnectBattery, flowType: RadioRemote }, - { flowStep: ConnectionFlowStep.ConnectCable, flowType: RadioBridge }, - { - flowStep: ConnectionFlowStep.WebUsbFlashingTutorial, - flowType: RadioBridge, - }, - ]; -}; +const bluetoothFlow = ({ + isManualFlashing, + isRestartAgain, +}: { + isManualFlashing: boolean; + isRestartAgain: boolean; +}) => [ + { + flowStep: isRestartAgain + ? ConnectionFlowStep.ReconnectFailedTwice + : ConnectionFlowStep.Start, + flowType: ConnectionFlowType.Bluetooth, + }, + { + flowStep: ConnectionFlowStep.ConnectCable, + flowType: ConnectionFlowType.Bluetooth, + }, + // Only bluetooth mode has this fallback, the radio bridge mode requires working WebUSB. + { + flowStep: isManualFlashing + ? ConnectionFlowStep.ManualFlashingTutorial + : ConnectionFlowStep.WebUsbFlashingTutorial, + flowType: ConnectionFlowType.Bluetooth, + }, + { + flowStep: ConnectionFlowStep.ConnectBattery, + flowType: ConnectionFlowType.Bluetooth, + }, + { + flowStep: ConnectionFlowStep.EnterBluetoothPattern, + flowType: ConnectionFlowType.Bluetooth, + }, + { + flowStep: ConnectionFlowStep.ConnectBluetoothTutorial, + flowType: ConnectionFlowType.Bluetooth, + }, +]; + +const radioFlow = () => [ + { + flowStep: ConnectionFlowStep.Start, + flowType: ConnectionFlowType.RadioRemote, + }, + { + flowStep: ConnectionFlowStep.ConnectCable, + flowType: ConnectionFlowType.RadioRemote, + }, + { + flowStep: ConnectionFlowStep.WebUsbFlashingTutorial, + flowType: ConnectionFlowType.RadioRemote, + }, + { + flowStep: ConnectionFlowStep.ConnectBattery, + flowType: ConnectionFlowType.RadioRemote, + }, + { + flowStep: ConnectionFlowStep.ConnectCable, + flowType: ConnectionFlowType.RadioBridge, + }, + { + flowStep: ConnectionFlowStep.WebUsbFlashingTutorial, + flowType: ConnectionFlowType.RadioBridge, + }, +]; const getFlowStageIdx = ( { flowStep, flowType }: FlowStage, @@ -324,12 +365,15 @@ const getFlowStageIdx = ( throw new Error("Should be able to match stage and type again order"); }; -const getNextStage = (stage: ConnectionStage, increment: number): FlowStage => { - const order = getStagesOrder(stage); - const currIdx = getFlowStageIdx(stage, order); +const getNextStage = ( + stage: ConnectionStage, + increment: number, + stagesOrder: FlowStage[] +): FlowStage => { + const currIdx = getFlowStageIdx(stage, stagesOrder); const newIdx = currIdx + increment; - if (newIdx === order.length || newIdx < 0) { + if (newIdx === stagesOrder.length || newIdx < 0) { throw new Error("Impossible step stage"); } - return order[newIdx]; + return stagesOrder[newIdx]; }; diff --git a/src/connection-stage-hooks.tsx b/src/connection-stage-hooks.tsx index c455ca8d8..644521ce9 100644 --- a/src/connection-stage-hooks.tsx +++ b/src/connection-stage-hooks.tsx @@ -11,6 +11,11 @@ import { ConnectActions } from "./connect-actions"; import { useConnectActions } from "./connect-actions-hooks"; import { ConnectionStageActions } from "./connection-stage-actions"; import { useStorage } from "./hooks/use-storage"; +import { + ConnectionStatus, + useConnectStatus, + useSetConnectStatus, +} from "./connect-status-hooks"; export enum ConnectionFlowType { Bluetooth = "bluetooth", @@ -18,14 +23,6 @@ export enum ConnectionFlowType { RadioRemote = "remote", } -export enum ConnectionStatus { - None = "None", // Have not been connected before - Connecting = "Connecting", - Connected = "Connected", - Disconnected = "Disconnected", - Reconnecting = "Reconnecting", -} - export type ConnectionType = "bluetooth" | "radio"; export enum ConnectionFlowStep { @@ -54,8 +51,8 @@ export enum ConnectionFlowStep { MicrobitUnsupported = "MicrobitUnsupported", WebUsbBluetoothUnsupported = "WebUsbBluetoothUnsupported", - ReconnectAutoFail = "ReconnectAutoFail", - ReconnectManualFail = "ReconnectManualFail", + ConnectionLost = "ConnectionLoss", + ReconnectFailed = "ReconnectFailed", ReconnectFailedTwice = "ReconnectFailedTwice", } @@ -63,21 +60,18 @@ export interface ConnectionStage { // For connection flow flowStep: ConnectionFlowStep; flowType: ConnectionFlowType; - // Number of times there have been consecutive reconnect fails - // for determining which reconnection dialog to show - reconnectFailStreak: number; // Compatibility isWebBluetoothSupported: boolean; isWebUsbSupported: boolean; // Connection state - status: ConnectionStatus; connType: "bluetooth" | "radio"; bluetoothDeviceId?: number; bluetoothMicrobitName?: string; radioBridgeDeviceId?: number; radioRemoteDeviceId?: number; + hasFailedToReconnectTwice: boolean; } type ConnectionStageContextValue = [ @@ -97,12 +91,11 @@ const getInitialConnectionStageValue = ( ): ConnectionStage => ({ flowStep: ConnectionFlowStep.None, flowType: ConnectionFlowType.Bluetooth, - reconnectFailStreak: 0, - status: ConnectionStatus.None, bluetoothMicrobitName: microbitName, connType: "bluetooth", isWebBluetoothSupported: true, isWebUsbSupported: true, + hasFailedToReconnectTwice: false, }); export const ConnectionStageProvider = ({ @@ -144,20 +137,20 @@ export const useConnectionStage = (): { const [stage, setStage] = connectionStageContextValue; const navigate = useNavigate(); const connectActions = useConnectActions(); + const setStatus = useSetConnectStatus(); const actions = useMemo(() => { return new ConnectionStageActions( connectActions, navigate, stage, - setStage + setStage, + setStatus ); - }, [connectActions, navigate, stage, setStage]); + }, [connectActions, navigate, stage, setStage, setStatus]); - const isConnected = useMemo( - () => stage.status === ConnectionStatus.Connected, - [stage.status] - ); + const status = useConnectStatus(actions.handleConnectionStatus); + const isConnected = status === ConnectionStatus.Connected; return { stage,