Skip to content
Open
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
8 changes: 8 additions & 0 deletions packages/hw-ble/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
root: true,
extends: ["@cypherock/eslint-config"],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json']
},
};
2 changes: 2 additions & 0 deletions packages/hw-ble/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.turbo
dist
1 change: 1 addition & 0 deletions packages/hw-ble/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"@cypherock/prettier-config"
5 changes: 5 additions & 0 deletions packages/hw-ble/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"plugins": [
"react-native-ble-plx"
]
}
100 changes: 100 additions & 0 deletions packages/hw-ble/docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# BLE DeviceConnection

Package name: `@cypherock/sdk-hw-ble`

This package allows you to connect with the Cypherock X1
hardware wallet via the BLE adapter.

Supported Platform includes `React Native (Android)` and
`React Native (iOS)`

## 1. Usage

Install packages: `npm i @cypherock/sdk-hw-ble`

```ts
import { DeviceConnection } from '@cypherock/sdk-hw-ble';

const connection = new DeviceConnection();
// Acquire appropriate permissions: DeviceConnection.getAndroidPermissionList()
await connection.startScanning();
await connection.connect(connection.getAvailableDevices()[0]);
```

## 2. Static Methods

### 2.1. `DeviceConnection.getAndroidPermissionList()`

Returns a list of permissions to be acquired before the module
is used.

**Arguments**: `None`

**Result**: `Permission[]`

**Example:**

```ts
const perms = DeviceConnection.getAndroidPermissionList();
perms.forEach(async perm => await PermissionsAndroid.request(perm));
```

## 3. Methods

### 3.1. `async connection.isConnected()`

Returns if the device is connected

**Arguments**: `None`

**Result**: `Promise<boolean>`

**Example:**

```ts
console.log(await connection.isConnected());
```

### 3.2. `async connection.getDeviceState()`

Returns the state of the device.

**Arguments**: `None`

**Result**: `Promise<DeviceState>`

```
enum DeviceState {
BOOTLOADER,
INITIAL,
MAIN,
}
```

**Example:**

```ts
console.log(await connection.getDeviceState());
```

### 3.3. `async connection.destroy()`

Destroys the connection instance.

**NOTE**: Do not destroy the connection if you'll need to connect to the same
device again. Destroying and recreating connection on the same device may cause
issues on some platforms.

**Arguments**: `None`

**Result**: `Promise<void>`

**Example:**

```ts
await connection.destroy();
```

## 4. Methods you won't need in most cases

Documentation pending
1 change: 1 addition & 0 deletions packages/hw-ble/mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
site_name: '@cypherock/sdk-hw-ble'
46 changes: 46 additions & 0 deletions packages/hw-ble/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@cypherock/sdk-hw-ble",
"version": "0.0.25",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .ts,tsx,js,jsx,js src/ --fix",
"lint:check": "eslint --ext .ts,tsx,js,jsx,js src/",
"pretty": "prettier --write 'src/**/*.ts'",
"pretty:check": "prettier --check 'src/**/*.ts'",
"build": "rimraf dist && tsc -p tsconfig.json",
"pre-commit": "lint-staged"
},
"devDependencies": {
"@cypherock/eslint-config": "workspace:*",
"@cypherock/prettier-config": "workspace:^0.0.8",
"@cypherock/tsconfig": "workspace:*",
"@types/node": "18.11.18",
"@types/uuid": "^9.0.0",
"eslint": "^7.32.0",
"lint-staged": "^13.2.0",
"rimraf": "^4.1.2",
"typescript": "^5.9.3"
},
"peerDependencies": {
"expo": "*",
"expo-device": "*",
"react-native-ble-plx": "*"
},
"dependencies": {
"@cypherock/sdk-interfaces": "workspace:^0.0.16",
"@cypherock/sdk-utils": "workspace:^",
"react-native": "0.81.5",
"uuid": "^9.0.0"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --ext js,jsx,ts,tsx --quiet --fix --",
"prettier --write"
],
"*.{md,mdx,mjs,yml,yaml,css,json}": [
"prettier --write"
]
}
}
218 changes: 218 additions & 0 deletions packages/hw-ble/src/deviceConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/* eslint-disable class-methods-use-this */
import {
ConnectionTypeMap,
DeviceState,
IDeviceConnection,
PoolData,
} from '@cypherock/sdk-interfaces';
import * as ExpoDevice from 'expo-device';
import { PermissionsAndroid } from 'react-native';
import {
BleError,
BleManager,
Characteristic,
Device,
} from 'react-native-ble-plx';
import * as uuid from 'uuid';
import { logger } from './logger';

const NUS_SERVICE_UUID = '6E400001-B5A3-F393-E0A9-E50E24DCCA9E';
const NUS_RX_CHARACTERISTICS_UUID = '6E400002-B5A3-F393-E0A9-E50E24DCCA9E';
const NUS_TX_CHARACTERISTICS_UUID = '6E400003-B5A3-F393-E0A9-E50E24DCCA9E';

export default class DeviceConnection implements IDeviceConnection {
private readonly bleManager: BleManager;

private pool: PoolData[];

private availableDevices: Device[];

private connectedDevice: Device | null;

private readonly deviceState: DeviceState;

private readonly initialized: boolean;

private sequenceNumber: number;

constructor(mgr: BleManager) {
this.bleManager = mgr;
this.pool = [];
this.availableDevices = [];
this.connectedDevice = null;
this.deviceState = DeviceState.MAIN;
this.initialized = true;
this.sequenceNumber = 0;
}

public async getConnectionType() {
return ConnectionTypeMap.HID;
}

public static getAndroidPermissionList() {
const perms = [PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION];

if ((ExpoDevice.platformApiLevel ?? -1) >= 31) {
perms.push(
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
);
}

return perms;
}

public getAvailableDevices() {
return this.availableDevices;
}

public async startScanning() {
this.availableDevices = [];

await this.bleManager.startDeviceScan(null, null, (err, device) => {
if (err) {
logger.error('Failed to start BLE scan!');
logger.error(err);
return;
}

if (device?.name?.toUpperCase().includes('X1 BLE')) {
const isDuplicate = (devices: Device[], nextDevice: Device) =>
devices.findIndex(d => nextDevice.id === d.id) > -1;

if (!isDuplicate(this.availableDevices, device)) {
this.availableDevices = [...this.availableDevices, device];
}
}
});
}

public async stopScanning() {
try {
await this.bleManager.stopDeviceScan();
this.availableDevices = [];
} catch (e) {
logger.warn('Error while stopping device scan');
logger.warn(e);
}
}

private readonly onDataReceive = (
error: BleError | null,
characteristic: Characteristic | null,
): void => {
if (error) {
logger.error(error);
return;
}
if (!characteristic?.value) {
logger.warn('No Data was recieved');
return;
}

const rawData: PoolData = {
id: uuid.v4(),
data: new Uint8Array(Buffer.from(characteristic.value, 'base64')),
};
this.pool = [...this.pool, rawData];
};

public async connect(device: Device) {
try {
await this.stopScanning();
const deviceConnection = await this.bleManager.connectToDevice(device.id);
await deviceConnection.discoverAllServicesAndCharacteristics();

/* USB HID packets are 64 bytes (padded with zero if need be) thus
* we need to ensure Bluetooth's each maximum transmission unit (MTU)
* must be atleast 64 bytes (data) + 5 bytes (headers) = 69 bytes.
*/
await this.bleManager.requestMTUForDevice(device.id, 517);

this.connectedDevice = device;
device.monitorCharacteristicForService(
NUS_SERVICE_UUID,
NUS_TX_CHARACTERISTICS_UUID,
this.onDataReceive,
undefined,
'notification',
);
} catch (e) {
logger.error('Error while connecting to the device');
logger.error(e);
throw e;
}
}

public async getDeviceState() {
return this.deviceState;
}

public isInitialized() {
return this.initialized;
}

public async getNewSequenceNumber() {
this.sequenceNumber += 1;
return this.sequenceNumber;
}

public async getSequenceNumber() {
return this.sequenceNumber;
}

public async isConnected() {
if (this.connectedDevice && (await this.connectedDevice.isConnected())) {
return true;
}

return false;
}

public async destroy() {
try {
if (!(await this.isConnected())) return;
if (!this.connectedDevice) return; // Because typescript isn't smart enough, lol.

await this.bleManager.cancelDeviceConnection(this.connectedDevice.id);
this.connectedDevice = null;
} catch (error) {
logger.warn('Error while closing device connection');
logger.warn(error);
}
console.log('Destroying BLE Instance!');
}

/**
* Run this function before starting every operation on the device.
* TODO(pegvin) - Implement this function
*/
// eslint-disable-next-line
public async beforeOperation() {}

/**
* Run this function after every operation on the device.
* TODO(pegvin) - Implement this function
*/
// eslint-disable-next-line
public async afterOperation() {}

// TODO(pegvin) - Check for ACK or resend the data if required.
public async send(data: Uint8Array) {
const dataToWrite = [...data, ...new Array(64 - data.length).fill(0x00)];

this.connectedDevice?.writeCharacteristicWithoutResponseForService(
NUS_SERVICE_UUID,
NUS_RX_CHARACTERISTICS_UUID,
Buffer.from(dataToWrite).toString('base64'),
);
}

public async receive() {
return this.pool.shift()?.data;
}

public async peek() {
return [...this.pool];
}
}
6 changes: 6 additions & 0 deletions packages/hw-ble/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import DeviceConnection from './deviceConnection';

export { default as DeviceConnection } from './deviceConnection';
export { updateLogger } from './logger';

export default DeviceConnection;
Loading