From 4687d1c2405aa9a679fe54880ff0c34fa91f58c3 Mon Sep 17 00:00:00 2001 From: Grace Date: Thu, 25 Jul 2024 16:01:31 +0100 Subject: [PATCH 01/20] WIP Somewhat working except for connect fail when no device is selected --- src/components/ConnectContainerDialog.tsx | 1 + src/components/LiveGraphPanel.tsx | 18 ++-- src/components/WhatYouWillNeedDialog.tsx | 2 +- src/connect-actions-hooks.tsx | 83 ++++++++++++++++ src/connect-actions.ts | 28 +++++- src/connection-stage-actions.ts | 116 ++++++++++++---------- src/connection-stage-hooks.tsx | 12 ++- 7 files changed, 193 insertions(+), 67 deletions(-) diff --git a/src/components/ConnectContainerDialog.tsx b/src/components/ConnectContainerDialog.tsx index f0add80ce..a12526640 100644 --- a/src/components/ConnectContainerDialog.tsx +++ b/src/components/ConnectContainerDialog.tsx @@ -64,6 +64,7 @@ const ConnectContainerDialog = ({ + // TODO: Add reconnect link )} {onBackClick && ( diff --git a/src/components/LiveGraphPanel.tsx b/src/components/LiveGraphPanel.tsx index ed6f44461..027ae0f61 100644 --- a/src/components/LiveGraphPanel.tsx +++ b/src/components/LiveGraphPanel.tsx @@ -8,16 +8,18 @@ import { } from "../connection-stage-hooks"; import InfoToolTip from "./InfoToolTip"; import LiveGraph from "./LiveGraph"; +import { useConnectStatus } from "../connect-actions-hooks"; const LiveGraphPanel = () => { const { actions } = useConnectionStage(); - const { stage } = useConnectionStage(); + const status = useConnectStatus(); const parentPortalRef = useRef(null); const connectBtnConfig = useMemo( () => - stage.status === ConnectionStatus.None || - stage.status === ConnectionStatus.Connecting + status === ConnectionStatus.None || + status === ConnectionStatus.Connecting || + status === ConnectionStatus.FailedToReconnectTwice ? { textId: "footer.connectButton", onClick: actions.start, @@ -26,7 +28,7 @@ const LiveGraphPanel = () => { textId: "actions.reconnect", onClick: actions.reconnect, }, - [actions.reconnect, actions.start, stage] + [actions.reconnect, actions.start, status] ); return ( @@ -49,7 +51,7 @@ const LiveGraphPanel = () => { > - {stage.status === ConnectionStatus.Connected ? ( + {status === ConnectionStatus.Connected ? ( @@ -58,15 +60,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/WhatYouWillNeedDialog.tsx b/src/components/WhatYouWillNeedDialog.tsx index 0098db768..0410a5355 100644 --- a/src/components/WhatYouWillNeedDialog.tsx +++ b/src/components/WhatYouWillNeedDialog.tsx @@ -75,7 +75,7 @@ const WhatYouWillNeedDialog = ({ }`} headingId={ reconnect - ? `connectMB.${type}Start.heading` + ? `reconnectFailed.${type}Heading` : `connectMB.${type}Start.heading` } > diff --git a/src/connect-actions-hooks.tsx b/src/connect-actions-hooks.tsx index 7d1098a32..64ee6862e 100644 --- a/src/connect-actions-hooks.tsx +++ b/src/connect-actions-hooks.tsx @@ -1,17 +1,23 @@ import { + ConnectionStatus as DeviceConnectionStatus, + ConnectionStatusEvent, MicrobitWebBluetoothConnection, MicrobitWebUSBConnection, } from "@microbit/microbit-connection"; import { + MutableRefObject, ReactNode, createContext, + useCallback, useContext, useEffect, useMemo, useRef, + useState, } from "react"; import { ConnectActions } from "./connect-actions"; import { useLogging } from "./logging/logging-hooks"; +import { ConnectionStatus } from "./connection-stage-hooks"; interface ConnectContextValue { usb: MicrobitWebUSBConnection; @@ -62,3 +68,80 @@ export const useConnectActions = (): ConnectActions => { return connectActions; }; + +const getNextConnectionStatus = ( + status: ConnectionStatus, + deviceStatus: DeviceConnectionStatus, + prevDeviceStatus: DeviceConnectionStatus | null, + numReconnectAttempt: MutableRefObject +) => { + if (deviceStatus === DeviceConnectionStatus.CONNECTED) { + return ConnectionStatus.Connected; + } + if ( + numReconnectAttempt.current > 0 && + deviceStatus === DeviceConnectionStatus.DISCONNECTED + ) { + numReconnectAttempt.current = 0; + return ConnectionStatus.FailedToReconnectTwice; + } + if ( + deviceStatus === DeviceConnectionStatus.DISCONNECTED && + prevDeviceStatus === DeviceConnectionStatus.CONNECTING + ) { + numReconnectAttempt.current++; + return ConnectionStatus.FailedToReconnectManually; + } + if ( + deviceStatus === DeviceConnectionStatus.DISCONNECTED && + prevDeviceStatus === DeviceConnectionStatus.RECONNECTING + ) { + numReconnectAttempt.current++; + return ConnectionStatus.FailedToReconnectAutomatically; + } + if (deviceStatus === DeviceConnectionStatus.DISCONNECTED) { + return ConnectionStatus.Disconnected; + } + if ( + deviceStatus === DeviceConnectionStatus.RECONNECTING || + deviceStatus === DeviceConnectionStatus.CONNECTING + ) { + return ConnectionStatus.None === status + ? ConnectionStatus.Connecting + : ConnectionStatus.Reconnecting; + } + return status; +}; + +export const useConnectStatus = ( + handleStatus?: (status: ConnectionStatus) => void +): ConnectionStatus => { + const connectActions = useConnectActions(); + const prevDeviceStatus = useRef(null); + const numReconnectAttempt = useRef(0); + const [status, setStatus] = useState(ConnectionStatus.None); + + const set = useCallback((s: ConnectionStatus) => { + console.log(s); + setStatus(s); + }, []); + useEffect(() => { + const listener = ({ status: deviceStatus }: ConnectionStatusEvent) => { + const newStatus = getNextConnectionStatus( + status, + deviceStatus, + prevDeviceStatus.current, + numReconnectAttempt + ); + prevDeviceStatus.current = deviceStatus; + handleStatus && handleStatus(newStatus); + setStatus(newStatus); + }; + connectActions.addStatusListener("bluetooth", listener); + return () => { + connectActions.removeStatusListener("bluetooth", listener); + }; + }, [connectActions, handleStatus, set, status]); + + return status; +}; diff --git a/src/connect-actions.ts b/src/connect-actions.ts index 33fb558c3..3bdba0d78 100644 --- a/src/connect-actions.ts +++ b/src/connect-actions.ts @@ -1,11 +1,12 @@ import { AccelerometerDataEvent, + ConnectionStatusEvent, ConnectionStatus as DeviceConnectionStatus, DeviceError, MicrobitWebBluetoothConnection, MicrobitWebUSBConnection, } from "@microbit/microbit-connection"; -import { ConnectionFlowType } from "./connection-stage-hooks"; +import { ConnectionFlowType, ConnectionType } from "./connection-stage-hooks"; import { getFlashDataSource } from "./device/get-hex-file"; import { Logging } from "./logging/logging"; @@ -87,7 +88,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) { @@ -128,8 +129,8 @@ export class ConnectActions { connectBluetooth = async ( name: string | undefined ): Promise => { - await this.bluetooth.connect({ name }); - if (this.bluetooth.status === DeviceConnectionStatus.CONNECTED) { + const status = await this.bluetooth.connect({ name }); + if (status === DeviceConnectionStatus.CONNECTED) { return ConnectResult.Success; } return ConnectResult.ManualConnectFailed; @@ -163,8 +164,25 @@ export class ConnectActions { this.bluetooth?.removeEventListener(type, listener); }; - // TODO: Replace with real disconnect logic disconnect = async () => { await this.bluetooth.disconnect(); }; + + addStatusListener = ( + type: ConnectionType, + listener: (e: ConnectionStatusEvent) => void + ) => { + if (type === "bluetooth") { + this.bluetooth?.addEventListener("status", listener); + } + }; + + removeStatusListener = ( + type: ConnectionType, + listener: (e: ConnectionStatusEvent) => void + ) => { + if (type === "bluetooth") { + this.bluetooth?.removeEventListener("status", listener); + } + }; } diff --git a/src/connection-stage-actions.ts b/src/connection-stage-actions.ts index 17beb78ba..c7b43e67a 100644 --- a/src/connection-stage-actions.ts +++ b/src/connection-stage-actions.ts @@ -11,6 +11,7 @@ import { ConnectionFlowType, ConnectionStage, ConnectionStatus, + ConnectionType, } from "./connection-stage-hooks"; import { createStepPageUrl } from "./urls"; @@ -79,11 +80,8 @@ export class ConnectionStageActions { } case ConnectionFlowType.RadioBridge: { newStage = { - ...this.stage, - connType: "radio", - flowStep: ConnectionFlowStep.ConnectingMicrobits, + ...this.getConnectingStage("bluetooth"), radioBridgeDeviceId: deviceId, - status: this.getConnectingOrReconnectingStatus(), }; break; } @@ -137,16 +135,8 @@ 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 - ); - this.handleConnectResult(result); + this.setStage(this.getConnectingStage("bluetooth")); + await this.actions.connectBluetooth(this.stage.bluetoothMicrobitName); }; connectMicrobits = async () => { @@ -160,52 +150,33 @@ 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) { return this.onConnected(); } - const newReconnectFailStreak = - this.stage.status === ConnectionStatus.Reconnecting - ? this.stage.reconnectFailStreak + 1 - : this.stage.reconnectFailStreak; + this.handleConnectFail(); + }; - const nextFlowStep = this.getReconnectFailFlowStep( - newReconnectFailStreak, - result - ); + private handleConnectFail = () => { this.setStage({ ...this.stage, - reconnectFailStreak: newReconnectFailStreak, status: ConnectionStatus.Disconnected, - flowStep: nextFlowStep, - }); - }; - - private getReconnectFailFlowStep = ( - failStreak: number, - result: ConnectResult - ) => { - switch (failStreak) { - case 0: { - return this.stage.flowType === ConnectionFlowType.Bluetooth + flowStep: + this.stage.flowType === ConnectionFlowType.Bluetooth ? ConnectionFlowStep.TryAgainBluetoothConnect - : ConnectionFlowStep.TryAgainReplugMicrobit; - } - case 1: { - return result === ConnectResult.ManualConnectFailed - ? ConnectionFlowStep.ReconnectManualFail - : ConnectionFlowStep.ReconnectAutoFail; - } - default: { - return ConnectionFlowStep.ReconnectFailedTwice; - } - } + : ConnectionFlowStep.TryAgainReplugMicrobit, + }); }; private onConnected = () => { @@ -226,10 +197,49 @@ export class ConnectionStageActions { }); }; + handleConnectionStatus = (status: ConnectionStatus) => { + console.log("handleConnectionStatus", status); + switch (status) { + case ConnectionStatus.Connected: { + return this.onConnected(); + } + case ConnectionStatus.FailedToConnect: { + return this.handleConnectFail(); + } + case ConnectionStatus.FailedToReconnectTwice: { + return this.setStage({ + ...this.stage, + flowStep: ConnectionFlowStep.ReconnectFailedTwice, + }); + } + case ConnectionStatus.FailedToReconnectManually: { + return this.setStage({ + ...this.stage, + flowStep: ConnectionFlowStep.ReconnectManualFail, + }); + } + case ConnectionStatus.FailedToReconnectAutomatically: { + return this.setStage({ + ...this.stage, + flowStep: ConnectionFlowStep.ReconnectAutoFail, + }); + } + case ConnectionStatus.Reconnecting: { + return this.setStage(this.getConnectingStage("bluetooth")); + } + case ConnectionStatus.Connecting: { + // Handled in connectingBluetooth for bluetooth and onFlashSuccess for radio + return; + } + } + return; + }; + reconnect = async () => { if (this.stage.connType === "bluetooth") { await this.connectBluetooth(); } else { + this.setStage(this.getConnectingStage("radio")); await this.connectMicrobits(); } }; @@ -273,7 +283,13 @@ const getStagesOrder = (state: ConnectionStage): FlowStage[] => { const { RadioRemote, RadioBridge, Bluetooth } = ConnectionFlowType; if (state.flowType === ConnectionFlowType.Bluetooth) { return [ - { flowStep: ConnectionFlowStep.Start, flowType: Bluetooth }, + { + flowStep: + state.flowStep === ConnectionFlowStep.ReconnectFailedTwice + ? ConnectionFlowStep.ReconnectFailedTwice + : ConnectionFlowStep.Start, + flowType: Bluetooth, + }, { flowStep: ConnectionFlowStep.ConnectCable, flowType: Bluetooth }, // Only bluetooth mode has this fallback, the radio bridge mode requires working WebUSB. { diff --git a/src/connection-stage-hooks.tsx b/src/connection-stage-hooks.tsx index c455ca8d8..6032ff35c 100644 --- a/src/connection-stage-hooks.tsx +++ b/src/connection-stage-hooks.tsx @@ -8,7 +8,7 @@ import { } from "react"; import { useNavigate } from "react-router"; import { ConnectActions } from "./connect-actions"; -import { useConnectActions } from "./connect-actions-hooks"; +import { useConnectActions, useConnectStatus } from "./connect-actions-hooks"; import { ConnectionStageActions } from "./connection-stage-actions"; import { useStorage } from "./hooks/use-storage"; @@ -24,6 +24,10 @@ export enum ConnectionStatus { Connected = "Connected", Disconnected = "Disconnected", Reconnecting = "Reconnecting", + FailedToConnect = "FailedToConnect", + FailedToReconnectManually = "FailedToReconnectManually", + FailedToReconnectAutomatically = "FailedToReconnectAutomatically", + FailedToReconnectTwice = "FailedToReconnectTwice", } export type ConnectionType = "bluetooth" | "radio"; @@ -154,9 +158,11 @@ export const useConnectionStage = (): { ); }, [connectActions, navigate, stage, setStage]); + const status = useConnectStatus(actions.handleConnectionStatus); + const isConnected = useMemo( - () => stage.status === ConnectionStatus.Connected, - [stage.status] + () => status === ConnectionStatus.Connected, + [status] ); return { From 70a5f69513715a5e72aa201c6515189210cfe661 Mon Sep 17 00:00:00 2001 From: Grace Date: Thu, 25 Jul 2024 16:16:48 +0100 Subject: [PATCH 02/20] Fix setting bluetooth connection name filter --- src/connect-actions.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/connect-actions.ts b/src/connect-actions.ts index 3bdba0d78..eeebc954e 100644 --- a/src/connect-actions.ts +++ b/src/connect-actions.ts @@ -129,7 +129,10 @@ export class ConnectActions { connectBluetooth = async ( name: string | undefined ): Promise => { - const status = await this.bluetooth.connect({ name }); + if (name !== undefined) { + this.bluetooth.setNameFilter(name); + } + const status = await this.bluetooth.connect(); if (status === DeviceConnectionStatus.CONNECTED) { return ConnectResult.Success; } From ed001d5d6a2925b978f1ff2ebb960afba7eca9cb Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 26 Jul 2024 11:26:07 +0100 Subject: [PATCH 03/20] Working version --- src/App.tsx | 17 ++-- src/components/LiveGraphPanel.tsx | 6 +- src/connect-actions-hooks.tsx | 83 ---------------- src/connect-actions.ts | 6 +- src/connect-status-hooks.tsx | 156 ++++++++++++++++++++++++++++++ src/connection-stage-actions.ts | 22 +++-- src/connection-stage-hooks.tsx | 16 ++- 7 files changed, 201 insertions(+), 105 deletions(-) create mode 100644 src/connect-status-hooks.tsx 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/LiveGraphPanel.tsx b/src/components/LiveGraphPanel.tsx index 027ae0f61..2ef37a41c 100644 --- a/src/components/LiveGraphPanel.tsx +++ b/src/components/LiveGraphPanel.tsx @@ -8,7 +8,7 @@ import { } from "../connection-stage-hooks"; import InfoToolTip from "./InfoToolTip"; import LiveGraph from "./LiveGraph"; -import { useConnectStatus } from "../connect-actions-hooks"; +import { useConnectStatus } from "../connect-status-hooks"; const LiveGraphPanel = () => { const { actions } = useConnectionStage(); @@ -17,8 +17,10 @@ const LiveGraphPanel = () => { const connectBtnConfig = useMemo( () => - status === ConnectionStatus.None || + status === ConnectionStatus.NotConnected || status === ConnectionStatus.Connecting || + status === ConnectionStatus.ChoosingDevice || + status === ConnectionStatus.FailedToConnect || status === ConnectionStatus.FailedToReconnectTwice ? { textId: "footer.connectButton", diff --git a/src/connect-actions-hooks.tsx b/src/connect-actions-hooks.tsx index 64ee6862e..7d1098a32 100644 --- a/src/connect-actions-hooks.tsx +++ b/src/connect-actions-hooks.tsx @@ -1,23 +1,17 @@ import { - ConnectionStatus as DeviceConnectionStatus, - ConnectionStatusEvent, MicrobitWebBluetoothConnection, MicrobitWebUSBConnection, } from "@microbit/microbit-connection"; import { - MutableRefObject, ReactNode, createContext, - useCallback, useContext, useEffect, useMemo, useRef, - useState, } from "react"; import { ConnectActions } from "./connect-actions"; import { useLogging } from "./logging/logging-hooks"; -import { ConnectionStatus } from "./connection-stage-hooks"; interface ConnectContextValue { usb: MicrobitWebUSBConnection; @@ -68,80 +62,3 @@ export const useConnectActions = (): ConnectActions => { return connectActions; }; - -const getNextConnectionStatus = ( - status: ConnectionStatus, - deviceStatus: DeviceConnectionStatus, - prevDeviceStatus: DeviceConnectionStatus | null, - numReconnectAttempt: MutableRefObject -) => { - if (deviceStatus === DeviceConnectionStatus.CONNECTED) { - return ConnectionStatus.Connected; - } - if ( - numReconnectAttempt.current > 0 && - deviceStatus === DeviceConnectionStatus.DISCONNECTED - ) { - numReconnectAttempt.current = 0; - return ConnectionStatus.FailedToReconnectTwice; - } - if ( - deviceStatus === DeviceConnectionStatus.DISCONNECTED && - prevDeviceStatus === DeviceConnectionStatus.CONNECTING - ) { - numReconnectAttempt.current++; - return ConnectionStatus.FailedToReconnectManually; - } - if ( - deviceStatus === DeviceConnectionStatus.DISCONNECTED && - prevDeviceStatus === DeviceConnectionStatus.RECONNECTING - ) { - numReconnectAttempt.current++; - return ConnectionStatus.FailedToReconnectAutomatically; - } - if (deviceStatus === DeviceConnectionStatus.DISCONNECTED) { - return ConnectionStatus.Disconnected; - } - if ( - deviceStatus === DeviceConnectionStatus.RECONNECTING || - deviceStatus === DeviceConnectionStatus.CONNECTING - ) { - return ConnectionStatus.None === status - ? ConnectionStatus.Connecting - : ConnectionStatus.Reconnecting; - } - return status; -}; - -export const useConnectStatus = ( - handleStatus?: (status: ConnectionStatus) => void -): ConnectionStatus => { - const connectActions = useConnectActions(); - const prevDeviceStatus = useRef(null); - const numReconnectAttempt = useRef(0); - const [status, setStatus] = useState(ConnectionStatus.None); - - const set = useCallback((s: ConnectionStatus) => { - console.log(s); - setStatus(s); - }, []); - useEffect(() => { - const listener = ({ status: deviceStatus }: ConnectionStatusEvent) => { - const newStatus = getNextConnectionStatus( - status, - deviceStatus, - prevDeviceStatus.current, - numReconnectAttempt - ); - prevDeviceStatus.current = deviceStatus; - handleStatus && handleStatus(newStatus); - setStatus(newStatus); - }; - connectActions.addStatusListener("bluetooth", listener); - return () => { - connectActions.removeStatusListener("bluetooth", listener); - }; - }, [connectActions, handleStatus, set, status]); - - return status; -}; diff --git a/src/connect-actions.ts b/src/connect-actions.ts index eeebc954e..a53902265 100644 --- a/src/connect-actions.ts +++ b/src/connect-actions.ts @@ -127,8 +127,12 @@ export class ConnectActions { }; connectBluetooth = async ( - name: string | undefined + name: string | undefined, + clearDevice: boolean ): Promise => { + if (clearDevice) { + await this.bluetooth.clearDevice(); + } if (name !== undefined) { this.bluetooth.setNameFilter(name); } diff --git a/src/connect-status-hooks.tsx b/src/connect-status-hooks.tsx new file mode 100644 index 000000000..19e589a9b --- /dev/null +++ b/src/connect-status-hooks.tsx @@ -0,0 +1,156 @@ +import { + ConnectionStatusEvent, + ConnectionStatus as DeviceConnectionStatus, +} from "@microbit/microbit-connection"; +import { + MutableRefObject, + ReactNode, + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { useConnectActions } from "./connect-actions-hooks"; +import { ConnectionStatus } from "./connection-stage-hooks"; + +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; + + const set = useCallback( + (s: ConnectionStatus) => { + console.log(s); + setStatus(s); + }, + [setStatus] + ); + + return set; +}; + +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); + + const set = useCallback( + (s: ConnectionStatus) => { + console.log(s); + setStatus(s); + }, + [setStatus] + ); + 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("bluetooth", listener); + return () => { + connectActions.removeStatusListener("bluetooth", listener); + }; + }, [connectActions, handleStatus, set, setStatus, status]); + + return status; +}; + +const getNextConnectionStatus = ( + status: ConnectionStatus, + deviceStatus: DeviceConnectionStatus, + prevDeviceStatus: DeviceConnectionStatus | null, + hasAttempedReconnect: MutableRefObject +) => { + if (deviceStatus === DeviceConnectionStatus.CONNECTED) { + hasAttempedReconnect.current = false; + return ConnectionStatus.Connected; + } + if ( + hasAttempedReconnect.current && + deviceStatus === DeviceConnectionStatus.DISCONNECTED && + prevDeviceStatus === DeviceConnectionStatus.CONNECTING + ) { + return ConnectionStatus.FailedToReconnectTwice; + } + if ( + deviceStatus === DeviceConnectionStatus.DISCONNECTED && + prevDeviceStatus === DeviceConnectionStatus.CONNECTING + ) { + hasAttempedReconnect.current = true; + return ConnectionStatus.FailedToReconnectManually; + } + if ( + deviceStatus === DeviceConnectionStatus.DISCONNECTED && + prevDeviceStatus === DeviceConnectionStatus.RECONNECTING + ) { + hasAttempedReconnect.current = true; + return ConnectionStatus.FailedToReconnectAutomatically; + } + if ( + (status === ConnectionStatus.Connecting && + deviceStatus === DeviceConnectionStatus.DISCONNECTED) || + (status === ConnectionStatus.ChoosingDevice && + deviceStatus === DeviceConnectionStatus.NO_AUTHORIZED_DEVICE) + ) { + return ConnectionStatus.FailedToConnect; + } + if (deviceStatus === DeviceConnectionStatus.DISCONNECTED) { + return ConnectionStatus.Disconnected; + } + if ( + deviceStatus === DeviceConnectionStatus.RECONNECTING || + deviceStatus === DeviceConnectionStatus.CONNECTING + ) { + return status === ConnectionStatus.ChoosingDevice || + status === ConnectionStatus.FailedToConnect + ? ConnectionStatus.Connecting + : ConnectionStatus.Reconnecting; + } + return undefined; +}; diff --git a/src/connection-stage-actions.ts b/src/connection-stage-actions.ts index c7b43e67a..217b7112e 100644 --- a/src/connection-stage-actions.ts +++ b/src/connection-stage-actions.ts @@ -22,10 +22,12 @@ 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, flowType: @@ -37,6 +39,7 @@ export class ConnectionStageActions { ? ConnectionFlowStep.WebUsbBluetoothUnsupported : ConnectionFlowStep.Start, }); + }; setFlowStep = (step: ConnectionFlowStep) => { this.setStage({ ...this.stage, flowStep: step }); @@ -134,9 +137,15 @@ export class ConnectionStageActions { }); }; - connectBluetooth = async () => { + connectBluetooth = async (clearDevice: boolean = true) => { this.setStage(this.getConnectingStage("bluetooth")); - await this.actions.connectBluetooth(this.stage.bluetoothMicrobitName); + if (clearDevice) { + this.setStatus(ConnectionStatus.ChoosingDevice); + } + await this.actions.connectBluetooth( + this.stage.bluetoothMicrobitName, + clearDevice + ); }; connectMicrobits = async () => { @@ -171,7 +180,6 @@ export class ConnectionStageActions { private handleConnectFail = () => { this.setStage({ ...this.stage, - status: ConnectionStatus.Disconnected, flowStep: this.stage.flowType === ConnectionFlowType.Bluetooth ? ConnectionFlowStep.TryAgainBluetoothConnect @@ -198,7 +206,7 @@ export class ConnectionStageActions { }; handleConnectionStatus = (status: ConnectionStatus) => { - console.log("handleConnectionStatus", status); + console.log(status); switch (status) { case ConnectionStatus.Connected: { return this.onConnected(); @@ -237,7 +245,7 @@ export class ConnectionStageActions { reconnect = async () => { if (this.stage.connType === "bluetooth") { - await this.connectBluetooth(); + await this.connectBluetooth(false); } else { this.setStage(this.getConnectingStage("radio")); await this.connectMicrobits(); diff --git a/src/connection-stage-hooks.tsx b/src/connection-stage-hooks.tsx index 6032ff35c..e56c05277 100644 --- a/src/connection-stage-hooks.tsx +++ b/src/connection-stage-hooks.tsx @@ -8,9 +8,10 @@ import { } from "react"; import { useNavigate } from "react-router"; import { ConnectActions } from "./connect-actions"; -import { useConnectActions, useConnectStatus } from "./connect-actions-hooks"; +import { useConnectActions } from "./connect-actions-hooks"; import { ConnectionStageActions } from "./connection-stage-actions"; import { useStorage } from "./hooks/use-storage"; +import { useConnectStatus, useSetConnectStatus } from "./connect-status-hooks"; export enum ConnectionFlowType { Bluetooth = "bluetooth", @@ -19,9 +20,12 @@ export enum ConnectionFlowType { } export enum ConnectionStatus { - None = "None", // Have not been connected before + // Initial / restarted connection state + NotConnected = "NotConnected", + ChoosingDevice = "ChoosingDevice", Connecting = "Connecting", Connected = "Connected", + // Have connected / attempted connection previously Disconnected = "Disconnected", Reconnecting = "Reconnecting", FailedToConnect = "FailedToConnect", @@ -102,7 +106,7 @@ const getInitialConnectionStageValue = ( flowStep: ConnectionFlowStep.None, flowType: ConnectionFlowType.Bluetooth, reconnectFailStreak: 0, - status: ConnectionStatus.None, + status: ConnectionStatus.NotConnected, bluetoothMicrobitName: microbitName, connType: "bluetooth", isWebBluetoothSupported: true, @@ -148,15 +152,17 @@ 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 status = useConnectStatus(actions.handleConnectionStatus); From 54e47a5283247b5e8c7ef7dea424fd22518ba94f Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 26 Jul 2024 11:29:22 +0100 Subject: [PATCH 04/20] Remove status from ConnectionStage in favour of using connection status hook instead --- src/connection-stage-actions.ts | 5 ----- src/connection-stage-hooks.tsx | 2 -- 2 files changed, 7 deletions(-) diff --git a/src/connection-stage-actions.ts b/src/connection-stage-actions.ts index 217b7112e..0c40da2db 100644 --- a/src/connection-stage-actions.ts +++ b/src/connection-stage-actions.ts @@ -191,7 +191,6 @@ export class ConnectionStageActions { this.setStage({ ...this.stage, flowStep: ConnectionFlowStep.None, - status: ConnectionStatus.Connected, reconnectFailStreak: 0, }); this.navigate(createStepPageUrl("add-data")); @@ -199,10 +198,6 @@ export class ConnectionStageActions { disconnect = async () => { await this.actions.disconnect(); - this.setStage({ - ...this.stage, - status: ConnectionStatus.Disconnected, - }); }; handleConnectionStatus = (status: ConnectionStatus) => { diff --git a/src/connection-stage-hooks.tsx b/src/connection-stage-hooks.tsx index e56c05277..436c3c659 100644 --- a/src/connection-stage-hooks.tsx +++ b/src/connection-stage-hooks.tsx @@ -80,7 +80,6 @@ export interface ConnectionStage { isWebUsbSupported: boolean; // Connection state - status: ConnectionStatus; connType: "bluetooth" | "radio"; bluetoothDeviceId?: number; bluetoothMicrobitName?: string; @@ -106,7 +105,6 @@ const getInitialConnectionStageValue = ( flowStep: ConnectionFlowStep.None, flowType: ConnectionFlowType.Bluetooth, reconnectFailStreak: 0, - status: ConnectionStatus.NotConnected, bluetoothMicrobitName: microbitName, connType: "bluetooth", isWebBluetoothSupported: true, From 9e99e04d3dd80bb98f9caa3017640e3f8c55f77c Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 26 Jul 2024 11:35:30 +0100 Subject: [PATCH 05/20] Remove reconnectFailStreak --- src/connection-stage-actions.ts | 38 ++++++++------------------------- src/connection-stage-hooks.tsx | 4 ---- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/src/connection-stage-actions.ts b/src/connection-stage-actions.ts index 0c40da2db..e26972f0f 100644 --- a/src/connection-stage-actions.ts +++ b/src/connection-stage-actions.ts @@ -178,21 +178,15 @@ export class ConnectionStageActions { }; private handleConnectFail = () => { - this.setStage({ - ...this.stage, - flowStep: - this.stage.flowType === ConnectionFlowType.Bluetooth - ? ConnectionFlowStep.TryAgainBluetoothConnect - : ConnectionFlowStep.TryAgainReplugMicrobit, - }); + this.setFlowStep( + this.stage.flowType === ConnectionFlowType.Bluetooth + ? ConnectionFlowStep.TryAgainBluetoothConnect + : ConnectionFlowStep.TryAgainReplugMicrobit + ); }; private onConnected = () => { - this.setStage({ - ...this.stage, - flowStep: ConnectionFlowStep.None, - reconnectFailStreak: 0, - }); + this.setFlowStep(ConnectionFlowStep.None); this.navigate(createStepPageUrl("add-data")); }; @@ -201,7 +195,6 @@ export class ConnectionStageActions { }; handleConnectionStatus = (status: ConnectionStatus) => { - console.log(status); switch (status) { case ConnectionStatus.Connected: { return this.onConnected(); @@ -210,30 +203,17 @@ export class ConnectionStageActions { return this.handleConnectFail(); } case ConnectionStatus.FailedToReconnectTwice: { - return this.setStage({ - ...this.stage, - flowStep: ConnectionFlowStep.ReconnectFailedTwice, - }); + return this.setFlowStep(ConnectionFlowStep.ReconnectFailedTwice); } case ConnectionStatus.FailedToReconnectManually: { - return this.setStage({ - ...this.stage, - flowStep: ConnectionFlowStep.ReconnectManualFail, - }); + return this.setFlowStep(ConnectionFlowStep.ReconnectManualFail); } case ConnectionStatus.FailedToReconnectAutomatically: { - return this.setStage({ - ...this.stage, - flowStep: ConnectionFlowStep.ReconnectAutoFail, - }); + return this.setFlowStep(ConnectionFlowStep.ReconnectAutoFail); } case ConnectionStatus.Reconnecting: { return this.setStage(this.getConnectingStage("bluetooth")); } - case ConnectionStatus.Connecting: { - // Handled in connectingBluetooth for bluetooth and onFlashSuccess for radio - return; - } } return; }; diff --git a/src/connection-stage-hooks.tsx b/src/connection-stage-hooks.tsx index 436c3c659..409bc7ac0 100644 --- a/src/connection-stage-hooks.tsx +++ b/src/connection-stage-hooks.tsx @@ -71,9 +71,6 @@ 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; @@ -104,7 +101,6 @@ const getInitialConnectionStageValue = ( ): ConnectionStage => ({ flowStep: ConnectionFlowStep.None, flowType: ConnectionFlowType.Bluetooth, - reconnectFailStreak: 0, bluetoothMicrobitName: microbitName, connType: "bluetooth", isWebBluetoothSupported: true, From 0849565ae7821fb1214c09d9aa2b615d0d84b3fc Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 26 Jul 2024 12:41:20 +0100 Subject: [PATCH 06/20] Move connection status to connect-status-hooks.tsx and document each status and what they mean. --- src/components/LiveGraphPanel.tsx | 9 ++--- src/connect-status-hooks.tsx | 55 ++++++++++++++++++++++++++++--- src/connection-stage-actions.ts | 8 ++--- src/connection-stage-hooks.tsx | 21 +++--------- 4 files changed, 62 insertions(+), 31 deletions(-) diff --git a/src/components/LiveGraphPanel.tsx b/src/components/LiveGraphPanel.tsx index 2ef37a41c..2d2dd5be8 100644 --- a/src/components/LiveGraphPanel.tsx +++ b/src/components/LiveGraphPanel.tsx @@ -2,13 +2,10 @@ import { Button, HStack, Portal, Text } from "@chakra-ui/react"; import { useMemo, useRef } from "react"; import { MdBolt } from "react-icons/md"; import { FormattedMessage } from "react-intl"; -import { - ConnectionStatus, - useConnectionStage, -} from "../connection-stage-hooks"; +import { useConnectionStage } from "../connection-stage-hooks"; import InfoToolTip from "./InfoToolTip"; import LiveGraph from "./LiveGraph"; -import { useConnectStatus } from "../connect-status-hooks"; +import { ConnectionStatus, useConnectStatus } from "../connect-status-hooks"; const LiveGraphPanel = () => { const { actions } = useConnectionStage(); @@ -19,7 +16,7 @@ const LiveGraphPanel = () => { () => status === ConnectionStatus.NotConnected || status === ConnectionStatus.Connecting || - status === ConnectionStatus.ChoosingDevice || + status === ConnectionStatus.ChoosingBluetoothDevice || status === ConnectionStatus.FailedToConnect || status === ConnectionStatus.FailedToReconnectTwice ? { diff --git a/src/connect-status-hooks.tsx b/src/connect-status-hooks.tsx index 19e589a9b..e25abb40f 100644 --- a/src/connect-status-hooks.tsx +++ b/src/connect-status-hooks.tsx @@ -13,7 +13,52 @@ import { useState, } from "react"; import { useConnectActions } from "./connect-actions-hooks"; -import { ConnectionStatus } from "./connection-stage-hooks"; + +export enum ConnectionStatus { + /** + * Represents the initial connection status. + */ + NotConnected = "NotConnected", + /** + * The user is choosing bluetooth device. + * Used for determining when user has not selected a device. + */ + ChoosingBluetoothDevice = "ChoosingDevice", + /** + * 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, @@ -123,19 +168,19 @@ const getNextConnectionStatus = ( prevDeviceStatus === DeviceConnectionStatus.CONNECTING ) { hasAttempedReconnect.current = true; - return ConnectionStatus.FailedToReconnectManually; + return ConnectionStatus.FailedToReconnect; } if ( deviceStatus === DeviceConnectionStatus.DISCONNECTED && prevDeviceStatus === DeviceConnectionStatus.RECONNECTING ) { hasAttempedReconnect.current = true; - return ConnectionStatus.FailedToReconnectAutomatically; + return ConnectionStatus.ConnectionLost; } if ( (status === ConnectionStatus.Connecting && deviceStatus === DeviceConnectionStatus.DISCONNECTED) || - (status === ConnectionStatus.ChoosingDevice && + (status === ConnectionStatus.ChoosingBluetoothDevice && deviceStatus === DeviceConnectionStatus.NO_AUTHORIZED_DEVICE) ) { return ConnectionStatus.FailedToConnect; @@ -147,7 +192,7 @@ const getNextConnectionStatus = ( deviceStatus === DeviceConnectionStatus.RECONNECTING || deviceStatus === DeviceConnectionStatus.CONNECTING ) { - return status === ConnectionStatus.ChoosingDevice || + return status === ConnectionStatus.ChoosingBluetoothDevice || status === ConnectionStatus.FailedToConnect ? ConnectionStatus.Connecting : ConnectionStatus.Reconnecting; diff --git a/src/connection-stage-actions.ts b/src/connection-stage-actions.ts index e26972f0f..ba9e8a2c6 100644 --- a/src/connection-stage-actions.ts +++ b/src/connection-stage-actions.ts @@ -10,10 +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; @@ -140,7 +140,7 @@ export class ConnectionStageActions { connectBluetooth = async (clearDevice: boolean = true) => { this.setStage(this.getConnectingStage("bluetooth")); if (clearDevice) { - this.setStatus(ConnectionStatus.ChoosingDevice); + this.setStatus(ConnectionStatus.ChoosingBluetoothDevice); } await this.actions.connectBluetooth( this.stage.bluetoothMicrobitName, @@ -205,10 +205,10 @@ export class ConnectionStageActions { case ConnectionStatus.FailedToReconnectTwice: { return this.setFlowStep(ConnectionFlowStep.ReconnectFailedTwice); } - case ConnectionStatus.FailedToReconnectManually: { + case ConnectionStatus.FailedToReconnect: { return this.setFlowStep(ConnectionFlowStep.ReconnectManualFail); } - case ConnectionStatus.FailedToReconnectAutomatically: { + case ConnectionStatus.ConnectionLost: { return this.setFlowStep(ConnectionFlowStep.ReconnectAutoFail); } case ConnectionStatus.Reconnecting: { diff --git a/src/connection-stage-hooks.tsx b/src/connection-stage-hooks.tsx index 409bc7ac0..49d19bc6b 100644 --- a/src/connection-stage-hooks.tsx +++ b/src/connection-stage-hooks.tsx @@ -11,7 +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 { useConnectStatus, useSetConnectStatus } from "./connect-status-hooks"; +import { + ConnectionStatus, + useConnectStatus, + useSetConnectStatus, +} from "./connect-status-hooks"; export enum ConnectionFlowType { Bluetooth = "bluetooth", @@ -19,21 +23,6 @@ export enum ConnectionFlowType { RadioRemote = "remote", } -export enum ConnectionStatus { - // Initial / restarted connection state - NotConnected = "NotConnected", - ChoosingDevice = "ChoosingDevice", - Connecting = "Connecting", - Connected = "Connected", - // Have connected / attempted connection previously - Disconnected = "Disconnected", - Reconnecting = "Reconnecting", - FailedToConnect = "FailedToConnect", - FailedToReconnectManually = "FailedToReconnectManually", - FailedToReconnectAutomatically = "FailedToReconnectAutomatically", - FailedToReconnectTwice = "FailedToReconnectTwice", -} - export type ConnectionType = "bluetooth" | "radio"; export enum ConnectionFlowStep { From d8977d8d50c521b5f1382e2de12cacd539388bbe Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 26 Jul 2024 13:02:06 +0100 Subject: [PATCH 07/20] Generalise bluetooth-specific parts --- src/components/LiveGraphPanel.tsx | 2 +- src/connect-actions.ts | 26 ++++++-------------------- src/connect-status-hooks.tsx | 12 ++++++------ src/connection-stage-actions.ts | 2 +- 4 files changed, 14 insertions(+), 28 deletions(-) diff --git a/src/components/LiveGraphPanel.tsx b/src/components/LiveGraphPanel.tsx index 2d2dd5be8..923092a5b 100644 --- a/src/components/LiveGraphPanel.tsx +++ b/src/components/LiveGraphPanel.tsx @@ -16,7 +16,7 @@ const LiveGraphPanel = () => { () => status === ConnectionStatus.NotConnected || status === ConnectionStatus.Connecting || - status === ConnectionStatus.ChoosingBluetoothDevice || + status === ConnectionStatus.ChoosingDevice || status === ConnectionStatus.FailedToConnect || status === ConnectionStatus.FailedToReconnectTwice ? { diff --git a/src/connect-actions.ts b/src/connect-actions.ts index 0ea17f8ef..a8e650502 100644 --- a/src/connect-actions.ts +++ b/src/connect-actions.ts @@ -121,18 +121,14 @@ export class ConnectActions { connectBluetooth = async ( name: string | undefined, clearDevice: boolean - ): Promise => { + ): Promise => { if (clearDevice) { await this.bluetooth.clearDevice(); } if (name) { this.bluetooth.setNameFilter(name); } - const status = await this.bluetooth.connect(); - if (status === DeviceConnectionStatus.CONNECTED) { - return ConnectResult.Success; - } - return ConnectResult.ManualConnectFailed; + await this.bluetooth.connect(); }; addAccelerometerListener = ( @@ -172,21 +168,11 @@ export class ConnectActions { await this.radioBridge.disconnect(); }; - addStatusListener = ( - type: ConnectionType, - listener: (e: ConnectionStatusEvent) => void - ) => { - if (type === "bluetooth") { - this.bluetooth?.addEventListener("status", listener); - } + addStatusListener = (listener: (e: ConnectionStatusEvent) => void) => { + this.bluetooth?.addEventListener("status", listener); }; - removeStatusListener = ( - type: ConnectionType, - listener: (e: ConnectionStatusEvent) => void - ) => { - if (type === "bluetooth") { - this.bluetooth?.removeEventListener("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 index e25abb40f..5ea8f8199 100644 --- a/src/connect-status-hooks.tsx +++ b/src/connect-status-hooks.tsx @@ -20,10 +20,10 @@ export enum ConnectionStatus { */ NotConnected = "NotConnected", /** - * The user is choosing bluetooth device. + * The user is choosing device. * Used for determining when user has not selected a device. */ - ChoosingBluetoothDevice = "ChoosingDevice", + ChoosingDevice = "ChoosingDevice", /** * Connecting occurs for the initial connection. */ @@ -137,9 +137,9 @@ export const useConnectStatus = ( setStatus(newStatus); } }; - connectActions.addStatusListener("bluetooth", listener); + connectActions.addStatusListener(listener); return () => { - connectActions.removeStatusListener("bluetooth", listener); + connectActions.removeStatusListener(listener); }; }, [connectActions, handleStatus, set, setStatus, status]); @@ -180,7 +180,7 @@ const getNextConnectionStatus = ( if ( (status === ConnectionStatus.Connecting && deviceStatus === DeviceConnectionStatus.DISCONNECTED) || - (status === ConnectionStatus.ChoosingBluetoothDevice && + (status === ConnectionStatus.ChoosingDevice && deviceStatus === DeviceConnectionStatus.NO_AUTHORIZED_DEVICE) ) { return ConnectionStatus.FailedToConnect; @@ -192,7 +192,7 @@ const getNextConnectionStatus = ( deviceStatus === DeviceConnectionStatus.RECONNECTING || deviceStatus === DeviceConnectionStatus.CONNECTING ) { - return status === ConnectionStatus.ChoosingBluetoothDevice || + return status === ConnectionStatus.ChoosingDevice || status === ConnectionStatus.FailedToConnect ? ConnectionStatus.Connecting : ConnectionStatus.Reconnecting; diff --git a/src/connection-stage-actions.ts b/src/connection-stage-actions.ts index ba9e8a2c6..934a53fe7 100644 --- a/src/connection-stage-actions.ts +++ b/src/connection-stage-actions.ts @@ -140,7 +140,7 @@ export class ConnectionStageActions { connectBluetooth = async (clearDevice: boolean = true) => { this.setStage(this.getConnectingStage("bluetooth")); if (clearDevice) { - this.setStatus(ConnectionStatus.ChoosingBluetoothDevice); + this.setStatus(ConnectionStatus.ChoosingDevice); } await this.actions.connectBluetooth( this.stage.bluetoothMicrobitName, From 60393b229e8a941eeaad20c5bdb1dfd693739451 Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 26 Jul 2024 13:11:46 +0100 Subject: [PATCH 08/20] Fix lint --- src/connect-actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connect-actions.ts b/src/connect-actions.ts index a8e650502..31fc938e5 100644 --- a/src/connect-actions.ts +++ b/src/connect-actions.ts @@ -8,7 +8,7 @@ import { MicrobitWebBluetoothConnection, MicrobitWebUSBConnection, } from "@microbit/microbit-connection"; -import { ConnectionFlowType, ConnectionType } from "./connection-stage-hooks"; +import { ConnectionFlowType } from "./connection-stage-hooks"; import { getFlashDataSource } from "./device/get-hex-file"; import { Logging } from "./logging/logging"; From 9fa55f7368400b8c2ae39dad36a8d254be8f2620 Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 26 Jul 2024 14:38:55 +0100 Subject: [PATCH 09/20] Add reconnect troubleshoot link for whatYouWillNeedDialog --- src/components/ConnectCableDialog.tsx | 12 ++++++---- src/components/ConnectContainerDialog.tsx | 15 ++++--------- src/components/WhatYouWillNeedDialog.tsx | 27 +++++++++++++++++++---- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/components/ConnectCableDialog.tsx b/src/components/ConnectCableDialog.tsx index 5e17e1d84..25966542f 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,13 @@ const ConnectCableDialog = ({ }; return ( + + + } > diff --git a/src/components/ConnectContainerDialog.tsx b/src/components/ConnectContainerDialog.tsx index a12526640..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,15 +55,10 @@ const ConnectContainerDialog = ({ - {onLinkClick && linkTextId && ( - - // TODO: Add reconnect link - )} + {footerLeft && footerLeft} {onBackClick && ( + )} + {reconnect && ( + + )} + + } > {reconnect && ( From d24802d73aef32da8c2fdefdba6e578f6bca953e Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 26 Jul 2024 14:40:57 +0100 Subject: [PATCH 10/20] Rename connection flow steps to connection loss and reconnect failed --- src/components/ConnectionFlowDialogs.tsx | 4 ++-- src/components/ReconnectErrorDialog.tsx | 8 ++++---- src/connection-stage-actions.ts | 4 ++-- src/connection-stage-hooks.tsx | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/ConnectionFlowDialogs.tsx b/src/components/ConnectionFlowDialogs.tsx index cbaf6fd9f..6be998580 100644 --- a/src/components/ConnectionFlowDialogs.tsx +++ b/src/components/ConnectionFlowDialogs.tsx @@ -236,8 +236,8 @@ const ConnectionDialogs = () => { return ; } // TODO: Reconnect dialogs - case ConnectionFlowStep.ReconnectManualFail: - case ConnectionFlowStep.ReconnectAutoFail: { + case ConnectionFlowStep.ReconnectFailed: + case ConnectionFlowStep.ConnectionLost: { return ( 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/connection-stage-actions.ts b/src/connection-stage-actions.ts index 934a53fe7..d7c7cdc5b 100644 --- a/src/connection-stage-actions.ts +++ b/src/connection-stage-actions.ts @@ -206,10 +206,10 @@ export class ConnectionStageActions { return this.setFlowStep(ConnectionFlowStep.ReconnectFailedTwice); } case ConnectionStatus.FailedToReconnect: { - return this.setFlowStep(ConnectionFlowStep.ReconnectManualFail); + return this.setFlowStep(ConnectionFlowStep.ReconnectFailed); } case ConnectionStatus.ConnectionLost: { - return this.setFlowStep(ConnectionFlowStep.ReconnectAutoFail); + return this.setFlowStep(ConnectionFlowStep.ConnectionLost); } case ConnectionStatus.Reconnecting: { return this.setStage(this.getConnectingStage("bluetooth")); diff --git a/src/connection-stage-hooks.tsx b/src/connection-stage-hooks.tsx index 49d19bc6b..e178f082d 100644 --- a/src/connection-stage-hooks.tsx +++ b/src/connection-stage-hooks.tsx @@ -51,8 +51,8 @@ export enum ConnectionFlowStep { MicrobitUnsupported = "MicrobitUnsupported", WebUsbBluetoothUnsupported = "WebUsbBluetoothUnsupported", - ReconnectAutoFail = "ReconnectAutoFail", - ReconnectManualFail = "ReconnectManualFail", + ConnectionLost = "ConnectionLoss", + ReconnectFailed = "ReconnectFailed", ReconnectFailedTwice = "ReconnectFailedTwice", } From eaca13d8b3d293211d7957764d0247ebe853020e Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 26 Jul 2024 16:15:01 +0100 Subject: [PATCH 11/20] Update how the user has not selected a device is determined to use deviceConnectionStatus instead of introducing ConnectionStatus.ChoosingDevice --- src/components/LiveGraphPanel.tsx | 1 - src/connect-status-hooks.tsx | 12 ++++-------- src/connection-stage-actions.ts | 3 --- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/components/LiveGraphPanel.tsx b/src/components/LiveGraphPanel.tsx index 923092a5b..6b7d51a35 100644 --- a/src/components/LiveGraphPanel.tsx +++ b/src/components/LiveGraphPanel.tsx @@ -16,7 +16,6 @@ const LiveGraphPanel = () => { () => status === ConnectionStatus.NotConnected || status === ConnectionStatus.Connecting || - status === ConnectionStatus.ChoosingDevice || status === ConnectionStatus.FailedToConnect || status === ConnectionStatus.FailedToReconnectTwice ? { diff --git a/src/connect-status-hooks.tsx b/src/connect-status-hooks.tsx index 5ea8f8199..0d065de89 100644 --- a/src/connect-status-hooks.tsx +++ b/src/connect-status-hooks.tsx @@ -19,11 +19,6 @@ export enum ConnectionStatus { * Represents the initial connection status. */ NotConnected = "NotConnected", - /** - * The user is choosing device. - * Used for determining when user has not selected a device. - */ - ChoosingDevice = "ChoosingDevice", /** * Connecting occurs for the initial connection. */ @@ -180,8 +175,9 @@ const getNextConnectionStatus = ( if ( (status === ConnectionStatus.Connecting && deviceStatus === DeviceConnectionStatus.DISCONNECTED) || - (status === ConnectionStatus.ChoosingDevice && - deviceStatus === DeviceConnectionStatus.NO_AUTHORIZED_DEVICE) + // If user does not select a device + (deviceStatus === DeviceConnectionStatus.NO_AUTHORIZED_DEVICE && + prevDeviceStatus === DeviceConnectionStatus.NO_AUTHORIZED_DEVICE) ) { return ConnectionStatus.FailedToConnect; } @@ -192,7 +188,7 @@ const getNextConnectionStatus = ( deviceStatus === DeviceConnectionStatus.RECONNECTING || deviceStatus === DeviceConnectionStatus.CONNECTING ) { - return status === ConnectionStatus.ChoosingDevice || + return status === ConnectionStatus.NotConnected || status === ConnectionStatus.FailedToConnect ? ConnectionStatus.Connecting : ConnectionStatus.Reconnecting; diff --git a/src/connection-stage-actions.ts b/src/connection-stage-actions.ts index d7c7cdc5b..a4a4eca73 100644 --- a/src/connection-stage-actions.ts +++ b/src/connection-stage-actions.ts @@ -139,9 +139,6 @@ export class ConnectionStageActions { connectBluetooth = async (clearDevice: boolean = true) => { this.setStage(this.getConnectingStage("bluetooth")); - if (clearDevice) { - this.setStatus(ConnectionStatus.ChoosingDevice); - } await this.actions.connectBluetooth( this.stage.bluetoothMicrobitName, clearDevice From fd1381b90fa18fbd08e9897d0871d9227d4b7c0c Mon Sep 17 00:00:00 2001 From: Grace Date: Fri, 26 Jul 2024 17:29:54 +0100 Subject: [PATCH 12/20] Remove debugging console logs --- src/connect-status-hooks.tsx | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/connect-status-hooks.tsx b/src/connect-status-hooks.tsx index 0d065de89..49989e482 100644 --- a/src/connect-status-hooks.tsx +++ b/src/connect-status-hooks.tsx @@ -6,7 +6,6 @@ import { MutableRefObject, ReactNode, createContext, - useCallback, useContext, useEffect, useRef, @@ -88,15 +87,7 @@ export const useSetConnectStatus = (): ((status: ConnectionStatus) => void) => { } const [, setStatus] = connectStatusContextValue; - const set = useCallback( - (s: ConnectionStatus) => { - console.log(s); - setStatus(s); - }, - [setStatus] - ); - - return set; + return setStatus; }; export const useConnectStatus = ( @@ -111,13 +102,6 @@ export const useConnectStatus = ( const prevDeviceStatus = useRef(null); const hasAttempedReconnect = useRef(false); - const set = useCallback( - (s: ConnectionStatus) => { - console.log(s); - setStatus(s); - }, - [setStatus] - ); useEffect(() => { const listener = ({ status: deviceStatus }: ConnectionStatusEvent) => { const newStatus = getNextConnectionStatus( @@ -136,7 +120,7 @@ export const useConnectStatus = ( return () => { connectActions.removeStatusListener(listener); }; - }, [connectActions, handleStatus, set, setStatus, status]); + }, [connectActions, handleStatus, setStatus, status]); return status; }; From a5a12dbb4c7c89d80467ba4a1bd121e14021582c Mon Sep 17 00:00:00 2001 From: Grace Date: Mon, 29 Jul 2024 09:16:33 +0100 Subject: [PATCH 13/20] Upgrade connection lib to ^0.0.0-alpha.14 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) 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", From 3099ff3fcf3939edd91456e1a3e03cdb0e9fc384 Mon Sep 17 00:00:00 2001 From: Grace <145345672+microbit-grace@users.noreply.github.com> Date: Mon, 29 Jul 2024 09:20:50 +0100 Subject: [PATCH 14/20] Remove no longer needed TODO comment --- src/components/ConnectionFlowDialogs.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ConnectionFlowDialogs.tsx b/src/components/ConnectionFlowDialogs.tsx index 6be998580..d50464544 100644 --- a/src/components/ConnectionFlowDialogs.tsx +++ b/src/components/ConnectionFlowDialogs.tsx @@ -235,7 +235,6 @@ const ConnectionDialogs = () => { case ConnectionFlowStep.WebUsbBluetoothUnsupported: { return ; } - // TODO: Reconnect dialogs case ConnectionFlowStep.ReconnectFailed: case ConnectionFlowStep.ConnectionLost: { return ( From 2e41c8f88347433cc8c497ddf9360b745018e044 Mon Sep 17 00:00:00 2001 From: Grace Date: Mon, 29 Jul 2024 10:53:27 +0100 Subject: [PATCH 15/20] Fix failed twice connection dialog flow so that going back will show correct dialog and fix connect vs reconnect btn state --- src/components/LiveGraphPanel.tsx | 24 +++-- src/connect-status-hooks.tsx | 8 ++ src/connection-stage-actions.ts | 153 +++++++++++++++++++----------- src/connection-stage-hooks.tsx | 2 + 4 files changed, 119 insertions(+), 68 deletions(-) diff --git a/src/components/LiveGraphPanel.tsx b/src/components/LiveGraphPanel.tsx index 6b7d51a35..7e6f1ff1a 100644 --- a/src/components/LiveGraphPanel.tsx +++ b/src/components/LiveGraphPanel.tsx @@ -12,22 +12,20 @@ const LiveGraphPanel = () => { const status = useConnectStatus(); const parentPortalRef = useRef(null); - const connectBtnConfig = useMemo( - () => - status === ConnectionStatus.NotConnected || + const connectBtnConfig = useMemo(() => { + return status === ConnectionStatus.NotConnected || status === ConnectionStatus.Connecting || status === ConnectionStatus.FailedToConnect || status === ConnectionStatus.FailedToReconnectTwice - ? { - textId: "footer.connectButton", - onClick: actions.start, - } - : { - textId: "actions.reconnect", - onClick: actions.reconnect, - }, - [actions.reconnect, actions.start, status] - ); + ? { + textId: "footer.connectButton", + onClick: actions.start, + } + : { + textId: "actions.reconnect", + onClick: actions.reconnect, + }; + }, [actions.reconnect, actions.start, status]); return ( ) => { + 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; diff --git a/src/connection-stage-actions.ts b/src/connection-stage-actions.ts index a4a4eca73..06926678b 100644 --- a/src/connection-stage-actions.ts +++ b/src/connection-stage-actions.ts @@ -30,6 +30,7 @@ export class ConnectionStageActions { this.setStatus(ConnectionStatus.NotConnected); this.setStage({ ...this.stage, + hasFailedToReconnectTwice: false, flowType: this.stage.flowType === ConnectionFlowType.RadioBridge ? ConnectionFlowType.RadioRemote @@ -200,7 +201,11 @@ export class ConnectionStageActions { return this.handleConnectFail(); } case ConnectionStatus.FailedToReconnectTwice: { - return this.setFlowStep(ConnectionFlowStep.ReconnectFailedTwice); + return this.setStage({ + ...this.stage, + hasFailedToReconnectTwice: true, + flowStep: ConnectionFlowStep.ReconnectFailedTwice, + }); } case ConnectionStatus.FailedToReconnect: { return this.setFlowStep(ConnectionFlowStep.ReconnectFailed); @@ -242,12 +247,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 = () => { @@ -259,53 +282,70 @@ export class ConnectionStageActions { }; } -const getStagesOrder = (state: ConnectionStage): FlowStage[] => { - const { RadioRemote, RadioBridge, Bluetooth } = ConnectionFlowType; - if (state.flowType === ConnectionFlowType.Bluetooth) { - return [ - { - flowStep: - state.flowStep === ConnectionFlowStep.ReconnectFailedTwice - ? ConnectionFlowStep.ReconnectFailedTwice - : 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, @@ -320,12 +360,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 e178f082d..6338ac71e 100644 --- a/src/connection-stage-hooks.tsx +++ b/src/connection-stage-hooks.tsx @@ -71,6 +71,7 @@ export interface ConnectionStage { bluetoothMicrobitName?: string; radioBridgeDeviceId?: number; radioRemoteDeviceId?: number; + hasFailedToReconnectTwice: boolean; } type ConnectionStageContextValue = [ @@ -94,6 +95,7 @@ const getInitialConnectionStageValue = ( connType: "bluetooth", isWebBluetoothSupported: true, isWebUsbSupported: true, + hasFailedToReconnectTwice: false, }); export const ConnectionStageProvider = ({ From 7e2d785b97d0c33e14cd375cc32e10c0ac990d04 Mon Sep 17 00:00:00 2001 From: Grace Date: Mon, 29 Jul 2024 11:05:04 +0100 Subject: [PATCH 16/20] Rearrange failed to conenct status change to be above reconnect --- src/connect-status-hooks.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/connect-status-hooks.tsx b/src/connect-status-hooks.tsx index 06bcf6dfb..4e7c7864c 100644 --- a/src/connect-status-hooks.tsx +++ b/src/connect-status-hooks.tsx @@ -143,6 +143,15 @@ const getNextConnectionStatus = ( 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 && @@ -164,15 +173,6 @@ const getNextConnectionStatus = ( hasAttempedReconnect.current = true; return ConnectionStatus.ConnectionLost; } - 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 (deviceStatus === DeviceConnectionStatus.DISCONNECTED) { return ConnectionStatus.Disconnected; } From b913c9c0de3c8dcf1cf01428a3c77d027209c440 Mon Sep 17 00:00:00 2001 From: Grace Date: Mon, 29 Jul 2024 12:38:34 +0100 Subject: [PATCH 17/20] Remove not needed "?" in connect-actions --- src/connect-actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/connect-actions.ts b/src/connect-actions.ts index 31fc938e5..6516ed0b0 100644 --- a/src/connect-actions.ts +++ b/src/connect-actions.ts @@ -169,10 +169,10 @@ export class ConnectActions { }; addStatusListener = (listener: (e: ConnectionStatusEvent) => void) => { - this.bluetooth?.addEventListener("status", listener); + this.bluetooth.addEventListener("status", listener); }; removeStatusListener = (listener: (e: ConnectionStatusEvent) => void) => { - this.bluetooth?.removeEventListener("status", listener); + this.bluetooth.removeEventListener("status", listener); }; } From 9a18509ddeb1ac32ada920d31eae106e542e6bff Mon Sep 17 00:00:00 2001 From: Grace Date: Mon, 29 Jul 2024 12:39:37 +0100 Subject: [PATCH 18/20] Use radio connecting stage instead of bt --- src/connection-stage-actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connection-stage-actions.ts b/src/connection-stage-actions.ts index 06926678b..d589e8f73 100644 --- a/src/connection-stage-actions.ts +++ b/src/connection-stage-actions.ts @@ -84,7 +84,7 @@ export class ConnectionStageActions { } case ConnectionFlowType.RadioBridge: { newStage = { - ...this.getConnectingStage("bluetooth"), + ...this.getConnectingStage("radio"), radioBridgeDeviceId: deviceId, }; break; From 9ecd2f445959c2b0efaf1e840caceee0c336d007 Mon Sep 17 00:00:00 2001 From: Grace Date: Mon, 29 Jul 2024 12:40:37 +0100 Subject: [PATCH 19/20] Calculate isConnected without using useMemo hook --- src/connection-stage-hooks.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/connection-stage-hooks.tsx b/src/connection-stage-hooks.tsx index 6338ac71e..644521ce9 100644 --- a/src/connection-stage-hooks.tsx +++ b/src/connection-stage-hooks.tsx @@ -150,11 +150,7 @@ export const useConnectionStage = (): { }, [connectActions, navigate, stage, setStage, setStatus]); const status = useConnectStatus(actions.handleConnectionStatus); - - const isConnected = useMemo( - () => status === ConnectionStatus.Connected, - [status] - ); + const isConnected = status === ConnectionStatus.Connected; return { stage, From b9120c3f32e5b4caf7c02f7f3849a518f586bf9e Mon Sep 17 00:00:00 2001 From: Grace Date: Mon, 29 Jul 2024 12:52:53 +0100 Subject: [PATCH 20/20] Fix radio connection happy flow --- src/components/ConnectCableDialog.tsx | 8 +++++--- src/connection-stage-actions.ts | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/ConnectCableDialog.tsx b/src/components/ConnectCableDialog.tsx index 25966542f..e641f5846 100644 --- a/src/components/ConnectCableDialog.tsx +++ b/src/components/ConnectCableDialog.tsx @@ -62,9 +62,11 @@ const ConnectCableDialog = ({ headingId={headingId} {...props} footerLeft={ - + linkTextId && ( + + ) } > diff --git a/src/connection-stage-actions.ts b/src/connection-stage-actions.ts index d589e8f73..e7a59fc7c 100644 --- a/src/connection-stage-actions.ts +++ b/src/connection-stage-actions.ts @@ -170,6 +170,11 @@ export class ConnectionStageActions { 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(); } this.handleConnectFail();