witmotion-react-native
Version:
React Native SDK for WitMotion BLE IMU sensors (WT901, WT901BLECL, etc.)
204 lines (203 loc) • 7.92 kB
JavaScript
/**
* WitMotion BLE Manager for React Native
*
* Provides:
* - Bluetooth permission handling
* - Device scanning
* - Connection management
* - Subscription to notifications
* - Command sending to WitMotion devices
*
* Uses react-native-ble-plx for BLE operations.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.WitCmd = exports.manager = void 0;
exports.ensureBlePermissions = ensureBlePermissions;
exports.scanForDevices = scanForDevices;
exports.connectById = connectById;
const react_native_ble_plx_1 = require("react-native-ble-plx");
const react_native_1 = require("react-native");
const Base64 = __importStar(require("base64-js"));
const witParser_1 = require("./witParser");
exports.manager = new react_native_ble_plx_1.BleManager();
/**
* Ensure required BLE permissions are granted.
* - Android 12+ requires BLUETOOTH_SCAN + BLUETOOTH_CONNECT
* - Android <12 requires ACCESS_FINE_LOCATION
*/
async function ensureBlePermissions() {
if (react_native_1.Platform.OS === 'android') {
if (react_native_1.Platform.Version >= 31) {
const res = await react_native_1.PermissionsAndroid.requestMultiple([
react_native_1.PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
react_native_1.PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
]);
const ok = res[react_native_1.PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN] === react_native_1.PermissionsAndroid.RESULTS.GRANTED
&& res[react_native_1.PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT] === react_native_1.PermissionsAndroid.RESULTS.GRANTED;
if (!ok)
throw new Error('BLE permissions denied');
}
else {
const ok = await react_native_1.PermissionsAndroid.request(react_native_1.PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);
if (ok !== react_native_1.PermissionsAndroid.RESULTS.GRANTED)
throw new Error('Location permission denied');
}
}
}
/**
* Ensure BLE state is powered on before use.
*/
async function ensureBleReady() {
const current = await exports.manager.state();
if (current === 'PoweredOn')
return;
await new Promise((resolve, reject) => {
const sub = exports.manager.onStateChange((state) => {
if (state === 'PoweredOn') {
sub.remove();
resolve();
}
if (state === 'Unsupported' || state === 'Unauthorized') {
sub.remove();
reject(new Error(`Bluetooth ${state}`));
}
}, true);
setTimeout(() => { sub.remove(); reject(new Error('Bluetooth not ready')); }, 10000);
});
}
/**
* Scan for nearby devices.
* @param onDevice callback fired for each device found
* @param ms duration of scan in milliseconds
*/
async function scanForDevices(onDevice, ms = 8000) {
await ensureBlePermissions();
await ensureBleReady();
const uuids = react_native_1.Platform.OS === 'ios' ? [] : null;
return new Promise((resolve) => {
exports.manager.startDeviceScan(uuids, { allowDuplicates: true }, (err, dev) => {
if (err) {
console.warn('Scan error:', err.message);
return;
}
if (!dev)
return;
onDevice({ id: dev.id, name: dev.name, rssi: dev.rssi });
});
setTimeout(() => { exports.manager.stopDeviceScan(); resolve(); }, ms);
});
}
/**
* Find the first writable characteristic of a device.
*/
async function findWrite(device) {
await device.discoverAllServicesAndCharacteristics();
const services = await device.services();
for (const s of services) {
const chars = await device.characteristicsForService(s.uuid);
for (const c of chars) {
if (c.isWritableWithResponse || c.isWritableWithoutResponse)
return { serviceUUID: s.uuid, characteristicUUID: c.uuid };
}
}
return null;
}
/**
* Subscribe to all notifiable characteristics of a device.
* Incoming data is streamed through WitStreamParser.
*/
async function subscribeAll(device, onData) {
const services = await device.services();
const parser = new witParser_1.WitStreamParser();
parser.onData = onData;
const subs = [];
for (const s of services) {
const chars = await device.characteristicsForService(s.uuid);
for (const c of chars) {
if (!c.isNotifiable)
continue;
const sub = device.monitorCharacteristicForService(s.uuid, c.uuid, (error, ch) => {
if (error) {
if (__DEV__)
console.log('Notify error', c.uuid, error.message);
return;
}
if (!ch?.value)
return;
const bytes = Base64.toByteArray(ch.value);
parser.push(bytes);
});
subs.push(sub);
}
}
if (!subs.length)
throw new Error('No notifiable characteristics found');
return subs;
}
/**
* Connect to a device by ID, subscribe to data, and get send() helper.
* @param deviceId target BLE device id
* @param onData callback invoked with parsed WitData
* @returns device instance, subscriptions, and send() helper
*/
async function connectById(deviceId, onData) {
await ensureBlePermissions();
await ensureBleReady();
const device = await exports.manager.connectToDevice(deviceId, { timeout: 8000 })
.catch(async () => exports.manager.connectToDevice(deviceId));
await device.discoverAllServicesAndCharacteristics();
const subs = await subscribeAll(device, onData);
const write = await findWrite(device);
const send = write ? async (bytes) => {
const base64 = Base64.fromByteArray(Uint8Array.from(bytes));
await device.writeCharacteristicWithResponseForService(write.serviceUUID, write.characteristicUUID, base64);
} : undefined;
return { device, subs, send };
}
/**
* Predefined WitMotion commands (mirroring the official SDK).
*/
exports.WitCmd = {
/** Start magnetometer calibration */
startMagCalib: [0xFF, 0xAA, 0x01, 0x07, 0x00],
/** Save current settings */
save: [0xFF, 0xAA, 0x00, 0x00, 0x00],
/** Reset yaw angle */
resetYaw: [0xFF, 0xAA, 0x01, 0x04, 0x00],
/** Set output rate in Hz */
setRateHz: (hz) => [0xFF, 0xAA, 0x03, hz & 0xFF, (hz >> 8) & 0xFF],
};
;