Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Bluetooth handle connection status for knownDevices #17221

Draft
wants to merge 5 commits into
base: feat/rust-bluetooth
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const nearbyDeviceA: BluetoothDevice = {
connected: false,
paired: false,
rssi: 0,
connectionStatus: { type: 'pairing' },
};

const nearbyDeviceC: BluetoothDevice = {
Expand All @@ -22,6 +23,7 @@ const nearbyDeviceC: BluetoothDevice = {
connected: false,
paired: false,
rssi: 0,
connectionStatus: { type: 'pairing' },
};

const knownDeviceB: BluetoothDevice = {
Expand All @@ -33,6 +35,7 @@ const knownDeviceB: BluetoothDevice = {
connected: false,
paired: false,
rssi: 0,
connectionStatus: { type: 'pairing' },
};

const knownDeviceA: BluetoothDevice = {
Expand All @@ -44,6 +47,7 @@ const knownDeviceA: BluetoothDevice = {
connected: false,
paired: false,
rssi: 0,
connectionStatus: { type: 'pairing' },
};

describe(remapKnownDevicesForLinux.name, () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BLUETOOTH_PREFIX, bluetoothActions } from '@suite-common/bluetooth';
import { BLUETOOTH_PREFIX } from '@suite-common/bluetooth';
import { createThunk } from '@suite-common/redux-utils';
import { notificationsActions } from '@suite-common/toast-notifications';
import { bluetoothIpc } from '@trezor/transport-bluetooth';
Expand All @@ -15,27 +15,15 @@ export const bluetoothConnectDeviceThunk = createThunk<
`${BLUETOOTH_PREFIX}/bluetoothConnectDeviceThunk`,
async ({ id }, { fulfillWithValue, dispatch }) => {
const result = await bluetoothIpc.connectDevice(id);
console.log('_____calling: bluetoothIpc.connectDevice(id)', id);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 reminder to remove console log.

Also, why remove connectDeviceEventAction? Is it guaranteed that when bluetoothIpc.connectDevice finishes, it will trigger deviceActions.connectDevice (so it'd be useless to call it twice), or how does it work?

if (!result.success) {
dispatch(
bluetoothActions.connectDeviceEventAction({
id,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change of the connection state is now responsibility of the bluetoothIpc and the connection status update will come from it as an update

connectionStatus: { type: 'error', error: result.error },
}),
);
dispatch(
notificationsActions.addToast({
type: 'error',
error: result.error,
}),
);
} else {
dispatch(
bluetoothActions.connectDeviceEventAction({
id,
connectionStatus: { type: 'connected' },
}),
);
}

return fulfillWithValue({ success: result.success });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { BLUETOOTH_PREFIX } from '@suite-common/bluetooth';
import { createThunk } from '@suite-common/redux-utils';
import { notificationsActions } from '@suite-common/toast-notifications';
import { bluetoothIpc } from '@trezor/transport-bluetooth';

type BluetoothDisconnectDeviceThunkResult = {
success: boolean;
};

export const bluetoothDisconnectDeviceThunk = createThunk<
BluetoothDisconnectDeviceThunkResult,
{ id: string },
void
>(
`${BLUETOOTH_PREFIX}/bluetoothConnectDeviceThunk`,
async ({ id }, { fulfillWithValue, dispatch }) => {
console.log('_____calling: bluetoothIpc.disconnectDevice(id)', id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as in bluetoothConnectDeviceThunk.ts

const result = await bluetoothIpc.disconnectDevice(id);

if (!result.success) {
dispatch(
notificationsActions.addToast({
type: 'error',
error: result.error,
}),
);
}

return fulfillWithValue({ success: result.success });
},
);
9 changes: 2 additions & 7 deletions packages/suite/src/actions/bluetooth/initBluetoothThunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,10 @@ export const initBluetoothThunk = createThunk<void, void, void>(
dispatch(bluetoothActions.nearbyDevicesUpdateAction({ nearbyDevices }));
});

bluetoothIpc.on('device-update', device => {
bluetoothIpc.on('device-update', (device: BluetoothDevice) => {
console.warn('device-update', device);

dispatch(
bluetoothActions.connectDeviceEventAction({
id: device.id,
connectionStatus: device.connectionStatus as any, // TODO: type
}),
);
dispatch(bluetoothActions.connectDeviceEventAction({ device }));
});

// TODO: this should be called after trezor/connect init?
Expand Down
1 change: 1 addition & 0 deletions packages/suite/src/actions/suite/storageActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const saveCoinjoinDebugSettings = () => async (_dispatch: Dispatch, getSt
export const saveKnownDevices = () => async (_dispatch: Dispatch, getState: GetState) => {
if (!(await db.isAccessible())) return;
const { knownDevices } = getState().bluetooth;
// Todo: consider adding serializeBluetoothDevice (do not save status, ... signal strength, ...)
db.addItem('knownDevices', { bluetooth: knownDevices }, 'devices', true);
};

Expand Down
63 changes: 28 additions & 35 deletions packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
selectKnownDevices,
selectScanStatus,
} from '@suite-common/bluetooth';
import { selectDevices } from '@suite-common/wallet-core';
import { Card, Column, ElevationUp } from '@trezor/components';
import { spacings } from '@trezor/theme';
import { BluetoothDevice } from '@trezor/transport-bluetooth';
Expand All @@ -21,10 +20,8 @@
import { BluetoothTips } from './BluetoothTips';
import { BluetoothNotEnabled } from './errors/BluetoothNotEnabled';
import { BluetoothVersionNotCompatible } from './errors/BluetoothVersionNotCompatible';
import { bluetoothConnectDeviceThunk } from '../../../actions/bluetooth/bluetoothConnectDeviceThunk';
import { bluetoothStartScanningThunk } from '../../../actions/bluetooth/bluetoothStartScanningThunk';
import { bluetoothStopScanningThunk } from '../../../actions/bluetooth/bluetoothStopScanningThunk';
import { closeModalApp } from '../../../actions/suite/routerActions';
import { useDispatch, useSelector } from '../../../hooks/suite';

const SCAN_TIMEOUT = 30_000;
Expand All @@ -43,38 +40,31 @@
const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
const [scannerTimerId, setScannerTimerId] = useState<TimerId | null>(null);

const trezorDevices = useSelector(selectDevices);

const bluetoothAdapterStatus = useSelector(selectAdapterStatus);
const scanStatus = useSelector(selectScanStatus);
const allDevices = useSelector(selectAllDevices);
const knownDevices = useSelector(selectKnownDevices);

Check failure on line 46 in packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

'knownDevices' is assigned a value but never used. Allowed unused vars must match /^_/u

const lasUpdatedBoundaryTimestamp =

Check failure on line 48 in packages/suite/src/components/suite/bluetooth/BluetoothConnect.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

'lasUpdatedBoundaryTimestamp' is assigned a value but never used. Allowed unused vars must match /^_/u
Date.now() / 1000 - UNPAIRED_DEVICES_LAST_UPDATED_LIMIT_SECONDS;

const devices = allDevices.filter(it => {
const isDeviceAlreadyConnected =
trezorDevices.find(trezorDevice => trezorDevice.bluetoothProps?.id === it.device.id) !==
undefined;

if (isDeviceAlreadyConnected) {
return false;
}

const isDeviceUnresponsiveForTooLong =
it.device.lastUpdatedTimestamp < lasUpdatedBoundaryTimestamp;

if (isDeviceUnresponsiveForTooLong) {
return knownDevices.find(knownDevice => knownDevice.id === it.device.id) !== undefined;
}
console.log('allDevices', allDevices);

return true;
});
const devices = allDevices;
// .filter(it => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be uncommented, I need a fix for this from Szymon

// const isDeviceUnresponsiveForTooLong =
// it.lastUpdatedTimestamp < lasUpdatedBoundaryTimestamp;
//
// if (isDeviceUnresponsiveForTooLong) {
// return knownDevices.find(knownDevice => knownDevice.id === it.id) !== undefined;
// }
//
// return true;
// });

const selectedDevice =
selectedDeviceId !== null
? devices.find(device => device.device.id === selectedDeviceId)
? devices.find(device => device.id === selectedDeviceId)
: undefined;

useEffect(() => {
Expand Down Expand Up @@ -111,13 +101,8 @@
setScannerTimerId(timerId);
};

const onSelect = async (id: string) => {
const onSelect = (id: string) => {
setSelectedDeviceId(id);
const result = await dispatch(bluetoothConnectDeviceThunk({ id })).unwrap();

if (uiMode === 'card' && result.success) {
dispatch(closeModalApp());
}
};

if (bluetoothAdapterStatus === 'disabled') {
Expand All @@ -143,21 +128,27 @@

if (
selectedDevice !== undefined &&
selectedDevice.status !== null &&
selectedDevice.status.type === 'pairing' &&
(selectedDevice.status.pin?.length ?? 0) > 0
selectedDevice !== null &&
selectedDevice.connectionStatus.type === 'pairing' &&
(selectedDevice.connectionStatus?.pin?.length ?? 0) > 0
) {
return (
<BluetoothPairingPin
device={selectedDevice.device}
pairingPin={selectedDevice.status.pin}
device={selectedDevice}
pairingPin={selectedDevice.connectionStatus.pin}
onCancel={handlePairingCancel}
/>
);
}

if (selectedDevice !== undefined) {
return <BluetoothSelectedDevice device={selectedDevice} onReScanClick={onReScanClick} />;
return (
<BluetoothSelectedDevice
device={selectedDevice}
onReScanClick={onReScanClick}
onCancel={handlePairingCancel}
/>
);
}

const content = scanFailed ? (
Expand All @@ -166,8 +157,10 @@
<BluetoothDeviceList
isDisabled={false}
onSelect={onSelect}
onError={handlePairingCancel}
deviceList={devices}
isScanning={isScanning}
uiMode={uiMode}
/>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import { useEffect, useState } from 'react';

import { Column, FlexProps, Icon, Row, Text } from '@trezor/components';
import { DeviceModelInternal } from '@trezor/connect';
import { models } from '@trezor/connect/src/data/models'; // Todo: solve this import issue
import { RotateDeviceImage } from '@trezor/product-components';
import { spacings } from '@trezor/theme';
import { BluetoothDevice } from '@trezor/transport-bluetooth';

const TimeAgo = ({ timestamp }: { timestamp: number }) => {
const [secAgo, setSecAgo] = useState(0);

useEffect(() => {
setSecAgo(Math.floor((Date.now() - timestamp) / 1000));
const interval = setInterval(() => setSecAgo(t => t + 1), 1000);

return () => clearInterval(interval);
}, [timestamp]);

return (
<>
<Text variant="warning">{secAgo}</Text>s ago
</>
);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be extracted as a reusable component..


type BluetoothDeviceProps = {
device: BluetoothDevice;
flex?: FlexProps['flex'];
Expand All @@ -23,7 +42,7 @@ export const BluetoothDeviceComponent = ({ device, flex, margin }: BluetoothDevi

const model = getModelEnumFromBytesUtil(device.data[2]);
const color = getColorEnumFromVariantBytesUtil(device.data[1]);
const colorName = models[model].colors[color.toString()];
const colorName = color !== undefined ? models[model].colors[color.toString()] : '';

return (
<Row gap={spacings.md} alignItems="stretch" flex={flex} margin={margin}>
Expand All @@ -36,7 +55,12 @@ export const BluetoothDeviceComponent = ({ device, flex, margin }: BluetoothDevi

<Column justifyContent="start" alignItems="start" flex="1">
<Text typographyStyle="body">Trezor Safe 7</Text>

<Text>
<Text typographyStyle="hint" variant="purple">
<pre>{device.macAddress}</pre>
</Text>
<TimeAgo timestamp={device.lastUpdatedTimestamp} />
</Text>
<Row>
<Text typographyStyle="hint" variant="tertiary">
{colorName}
Expand Down
Loading
Loading