Skip to content

Commit

Permalink
feat: prepare for usage of connectionStatus from ipc
Browse files Browse the repository at this point in the history
  • Loading branch information
peter-sanderson committed Feb 26, 2025
1 parent 20f538d commit 89455d9
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 187 deletions.
22 changes: 4 additions & 18 deletions packages/suite/src/actions/bluetooth/initBluetoothThunk.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { BLUETOOTH_PREFIX, bluetoothActions, selectKnownDevices } from '@suite-common/bluetooth';
import { createThunk } from '@suite-common/redux-utils/';
import { BluetoothDevice, DeviceConnectionStatus, bluetoothIpc } from '@trezor/transport-bluetooth';
import { Without } from '@trezor/type-utils';
import { BluetoothDevice, bluetoothIpc } from '@trezor/transport-bluetooth';

import { remapKnownDevicesForLinux } from './remapKnownDevicesForLinux';
import { selectSuiteFlags } from '../../reducers/suite/suiteReducer';

type DeviceConnectionStatusWithOptionalId = Without<DeviceConnectionStatus, 'id'> & {
id?: string;
};

export const initBluetoothThunk = createThunk<void, void, void>(
`${BLUETOOTH_PREFIX}/initBluetoothThunk`,
async (_, { dispatch, getState }) => {
Expand Down Expand Up @@ -40,19 +35,10 @@ export const initBluetoothThunk = createThunk<void, void, void>(
dispatch(bluetoothActions.nearbyDevicesUpdateAction({ nearbyDevices }));
});

bluetoothIpc.on('device-connection-status', connectionStatus => {
console.warn('device-connection-status', connectionStatus);
const copyConnectionStatus: DeviceConnectionStatusWithOptionalId = {
...connectionStatus,
};
delete copyConnectionStatus.id; // So we dont pollute redux store
bluetoothIpc.on('device-update', (device: BluetoothDevice) => {
console.warn('device-update', device);

dispatch(
bluetoothActions.connectDeviceEventAction({
id: connectionStatus.id,
connectionStatus: copyConnectionStatus,
}),
);
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
11 changes: 0 additions & 11 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 @@ import {
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 Down Expand Up @@ -43,8 +42,6 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) =>
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);
Expand All @@ -56,14 +53,6 @@ export const BluetoothConnect = ({ onClose, uiMode }: BluetoothConnectProps) =>
console.log('allDevices', allDevices);

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;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { BluetoothDeviceState, DeviceBluetoothStatusType } from '@suite-common/bluetooth';
import { BluetoothDeviceState, DeviceBluetoothConnectionStatusType } from '@suite-common/bluetooth';
import { Button, Row } from '@trezor/components';
import { spacings } from '@trezor/theme';
import { BluetoothDevice } from '@trezor/transport-bluetooth';

import { BluetoothDeviceComponent } from './BluetoothDeviceComponent';

const map: Record<DeviceBluetoothStatusType, string> = {
const map: Record<DeviceBluetoothConnectionStatusType, string> = {
connecting: 'Connecting',
connected: 'Connected',
error: 'Error',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ReactNode } from 'react';

import { BluetoothDeviceState, DeviceBluetoothStatusType } from '@suite-common/bluetooth';
import { BluetoothDeviceState, DeviceBluetoothConnectionStatusType } from '@suite-common/bluetooth';
import {
Button,
Card,
Expand Down Expand Up @@ -44,7 +44,7 @@ export type OkComponentProps = {
};

const OkComponent = ({ device, onCancel }: OkComponentProps) => {
const map: Record<DeviceBluetoothStatusType, ReactNode> = {
const map: Record<DeviceBluetoothConnectionStatusType, ReactNode> = {
connecting: <ConnectingComponent />,
connected: null,
error: null,
Expand Down
5 changes: 3 additions & 2 deletions packages/suite/src/middlewares/wallet/storageMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,10 @@ const storageMiddleware = (api: MiddlewareAPI<Dispatch, AppState>) => {
}

if (
deviceActions.connectDevice.match(action) ||
deviceActions.connectDevice.match(action) || // Known device is stored
bluetoothActions.knownDevicesUpdateAction.match(action) ||
bluetoothActions.removeKnownDeviceAction.match(action)
bluetoothActions.removeKnownDeviceAction.match(action) ||
bluetoothActions.connectDeviceEventAction.match(action) // Known devices may be updated
) {
api.dispatch(storageActions.saveKnownDevices());
}
Expand Down
10 changes: 3 additions & 7 deletions suite-common/bluetooth/src/bluetoothActions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { createAction } from '@reduxjs/toolkit';

import {
BluetoothDeviceCommon,
BluetoothScanStatus,
DeviceBluetoothStatus,
} from './bluetoothReducer';
import { BluetoothDeviceCommon, BluetoothScanStatus } from './bluetoothReducer';

export const BLUETOOTH_PREFIX = '@suite/bluetooth';

Expand Down Expand Up @@ -44,8 +40,8 @@ const removeKnownDeviceAction = createAction(

const connectDeviceEventAction = createAction(
`${BLUETOOTH_PREFIX}/connect-device-event`,
({ connectionStatus, id }: { id: string; connectionStatus: DeviceBluetoothStatus }) => ({
payload: { id, connectionStatus },
({ device }: { device: BluetoothDeviceCommon }) => ({
payload: { device },
}),
);

Expand Down
66 changes: 34 additions & 32 deletions suite-common/bluetooth/src/bluetoothReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ import { bluetoothActions } from './bluetoothActions';

export type BluetoothScanStatus = 'idle' | 'running' | 'error';

export type DeviceBluetoothStatus =
export type DeviceBluetoothConnectionStatus =
| { type: 'disconnected' }
| { type: 'pairing'; pin?: string }
| { type: 'paired' }
| { type: 'connecting' }
| { type: 'connected' }
| {
type: 'error';
type: 'pairing-error'; // This device cannot be paired ever again (new macAddress, new device)
error: string;
}
| {
type: 'connection-error'; // Out-of-range, offline, in the faraday cage, ...
error: string; // Timeout, connection aborted, ...
};

// Do not export this outside of this suite-common package, Suite uses ist own type
Expand All @@ -24,19 +29,15 @@ export type BluetoothDeviceCommon = {
name: string;
data: number[]; // Todo: consider typed data-structure for this
lastUpdatedTimestamp: number;
connectionStatus: DeviceBluetoothConnectionStatus;
};

export type DeviceBluetoothStatusType = DeviceBluetoothStatus['type'];

export type BluetoothDeviceState<T extends BluetoothDeviceCommon> = {
device: T;
status: DeviceBluetoothStatus | null;
};
export type DeviceBluetoothConnectionStatusType = DeviceBluetoothConnectionStatus['type'];

export type BluetoothState<T extends BluetoothDeviceCommon> = {
adapterStatus: 'unknown' | 'enabled' | 'disabled';
scanStatus: BluetoothScanStatus;
nearbyDevices: BluetoothDeviceState<T>[];
nearbyDevices: T[]; // Must be sorted, newest last

// This will be persisted, those are devices we believed that are paired
// (because we already successfully paired them in the Suite) in the Operating System
Expand All @@ -47,7 +48,7 @@ export const prepareBluetoothReducerCreator = <T extends BluetoothDeviceCommon>(
const initialState: BluetoothState<T> = {
adapterStatus: 'unknown',
scanStatus: 'idle',
nearbyDevices: [] as BluetoothDeviceState<T>[],
nearbyDevices: [] as T[],
knownDevices: [] as T[],
};

Expand All @@ -63,26 +64,21 @@ export const prepareBluetoothReducerCreator = <T extends BluetoothDeviceCommon>(
.addCase(
bluetoothActions.nearbyDevicesUpdateAction,
(state, { payload: { nearbyDevices } }) => {
state.nearbyDevices = nearbyDevices
.sort((a, b) => b.lastUpdatedTimestamp - a.lastUpdatedTimestamp)
.map(
(device): Draft<BluetoothDeviceState<T>> => ({
device: device as Draft<T>,
status:
state.nearbyDevices.find(it => it.device.id === device.id)
?.status ?? null,
}),
);
state.nearbyDevices = nearbyDevices.sort(
(a, b) => b.lastUpdatedTimestamp - a.lastUpdatedTimestamp,
) as Draft<T>[];
},
)
.addCase(
bluetoothActions.connectDeviceEventAction,
(state, { payload: { id, connectionStatus } }) => {
const device = state.nearbyDevices.find(it => it.device.id === id);
(state, { payload: { device } }) => {
state.nearbyDevices = state.nearbyDevices.map(it =>
it.id === device.id ? device : it,
) as Draft<T>[];

if (device !== undefined) {
device.status = connectionStatus;
}
state.knownDevices = state.knownDevices.map(it =>
it.id === device.id ? device : it,
) as Draft<T>[];
},
)
.addCase(
Expand All @@ -102,7 +98,7 @@ export const prepareBluetoothReducerCreator = <T extends BluetoothDeviceCommon>(
.addCase(deviceActions.deviceDisconnect, (state, { payload: { bluetoothProps } }) => {
if (bluetoothProps !== undefined) {
state.nearbyDevices = state.nearbyDevices.filter(
it => it.device.id !== bluetoothProps.id,
it => it.id !== bluetoothProps.id,
);
}
})
Expand All @@ -120,26 +116,32 @@ export const prepareBluetoothReducerCreator = <T extends BluetoothDeviceCommon>(
return;
}

const deviceState = state.nearbyDevices.find(
it => it.device.id === bluetoothProps.id,
);
const device = state.nearbyDevices.find(it => it.id === bluetoothProps.id);

if (deviceState !== undefined) {
if (device !== undefined) {
// Once device is fully connected, we save it to the list of known devices
// so next time user opens suite we can automatically connect to it.
const foundKnownDevice = state.knownDevices.find(
it => it.id === bluetoothProps.id,
);
if (foundKnownDevice === undefined) {
state.knownDevices.push(deviceState.device);
state.knownDevices.push(device);
}
}
},
)
.addMatcher(
action => action.type === extra.actionTypes.storageLoad,
(state, action: AnyAction) => {
state.knownDevices = action.payload.knownDevices?.bluetooth ?? [];
const loadedKnownDevices = (action.payload.knownDevices?.bluetooth ??
[]) as T[];

state.knownDevices = loadedKnownDevices.map(
(it): T => ({
...it,
status: { type: 'disconnected' },
}),
) as Draft<T>[];
},
),
);
Expand Down
28 changes: 12 additions & 16 deletions suite-common/bluetooth/src/bluetoothSelectors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createWeakMapSelector } from '@suite-common/redux-utils';

import { BluetoothDeviceCommon, BluetoothDeviceState, BluetoothState } from './bluetoothReducer';
import { BluetoothDeviceCommon, BluetoothState } from './bluetoothReducer';

export type WithBluetoothState<T extends BluetoothDeviceCommon> = {
bluetooth: BluetoothState<T>;
Expand All @@ -25,26 +25,22 @@ export const prepareSelectAllDevices = <T extends BluetoothDeviceCommon>() =>
createWeakMapSelector.withTypes<WithBluetoothState<T>>()(
[state => state.bluetooth.nearbyDevices, state => state.bluetooth.knownDevices],
(nearbyDevices, knownDevices) => {
const map = new Map<string, BluetoothDeviceState<T>>();
const map = new Map<string, T>();

knownDevices.forEach(knownDevice => map.set(knownDevice.id, knownDevice));

nearbyDevices
// Devices with error status should be displayed in the list, as it
// won't be possible to connect to them ever again. User has to start
// pairing again, which would produce a device with new id.
.filter(nearbyDevice => nearbyDevice.status?.type !== 'error')
.reverse()
// Devices with 'pairing-error' status should NOT be displayed in the list, as it
// won't be possible to connect to them ever again. User has to start pairing again,
// which would produce a device with new id.
.filter(nearbyDevice => nearbyDevice.connectionStatus?.type !== 'pairing-error')
.forEach(nearbyDevice => {
map.set(nearbyDevice.device.id, nearbyDevice);
map.delete(nearbyDevice.id); // Delete and re-add to change the order, replace would keep original order
map.set(nearbyDevice.id, nearbyDevice);
});

knownDevices.forEach(knownDevice => {
// All known devices are automatically considered being in 'connecting' state
// underlying code should connect them automatically.
map.set(knownDevice.id, { device: knownDevice, status: { type: 'connecting' } });
});

return Array.from(map.values()).sort(
(a, b) => b.device.lastUpdatedTimestamp - a.device.lastUpdatedTimestamp,
);
return Array.from(map.values());
},
);

Expand Down
3 changes: 1 addition & 2 deletions suite-common/bluetooth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ export { BLUETOOTH_PREFIX, bluetoothActions } from './bluetoothActions';

export { prepareBluetoothReducerCreator } from './bluetoothReducer';
export type {
BluetoothDeviceState,
BluetoothScanStatus,
DeviceBluetoothStatusType,
DeviceBluetoothConnectionStatusType,
} from './bluetoothReducer';

export {
Expand Down
Loading

0 comments on commit 89455d9

Please sign in to comment.