WellNuo/services/ble/BLEManager.ts
Sergei d453126c89 feat: Room location picker + robster credentials
- Backend: Update Legacy API credentials to robster/rob2
- Frontend: ROOM_LOCATIONS with icons and legacyCode mapping
- Device Settings: Modal picker for room selection
- api.ts: Bidirectional conversion (code ↔ name)
- Various UI/UX improvements across screens

PRD-DEPLOYMENT.md completed (Score: 9/10)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-24 15:22:40 -08:00

287 lines
8.4 KiB
TypeScript

// Real BLE Manager для физических устройств
import { BleManager, Device, State } from 'react-native-ble-plx';
import { PermissionsAndroid, Platform } from 'react-native';
import { IBLEManager, WPDevice, WiFiNetwork, WiFiStatus, BLE_CONFIG, BLE_COMMANDS } from './types';
import base64 from 'react-native-base64';
export class RealBLEManager implements IBLEManager {
private manager: BleManager;
private connectedDevices = new Map<string, Device>();
private scanning = false;
constructor() {
this.manager = new BleManager();
}
// Check and request permissions
private async requestPermissions(): Promise<boolean> {
if (Platform.OS === 'ios') {
// iOS handles permissions automatically via Info.plist
return true;
}
if (Platform.OS === 'android') {
if (Platform.Version >= 31) {
// Android 12+
const granted = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN!,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT!,
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION!,
]);
return Object.values(granted).every(
status => status === PermissionsAndroid.RESULTS.GRANTED
);
} else {
// Android < 12
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION!
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
}
}
return false;
}
// Check if Bluetooth is enabled
private async isBluetoothEnabled(): Promise<boolean> {
const state = await this.manager.state();
return state === State.PoweredOn;
}
async scanDevices(): Promise<WPDevice[]> {
const hasPermission = await this.requestPermissions();
if (!hasPermission) {
throw new Error('Bluetooth permissions not granted');
}
const isEnabled = await this.isBluetoothEnabled();
if (!isEnabled) {
throw new Error('Bluetooth is disabled. Please enable it in settings.');
}
const foundDevices = new Map<string, WPDevice>();
return new Promise((resolve, reject) => {
this.scanning = true;
this.manager.startDeviceScan(
null,
{ allowDuplicates: false },
(error, device) => {
if (error) {
this.scanning = false;
reject(error);
return;
}
if (device && device.name?.startsWith(BLE_CONFIG.DEVICE_NAME_PREFIX)) {
// Parse well_id from name (WP_497_81a14c -> 497)
const wellIdMatch = device.name.match(/WP_(\d+)_/);
const wellId = wellIdMatch ? parseInt(wellIdMatch[1], 10) : undefined;
// Extract MAC from device name (last part after underscore)
const macMatch = device.name.match(/_([a-fA-F0-9]{6})$/);
const mac = macMatch ? macMatch[1].toUpperCase() : '';
foundDevices.set(device.id, {
id: device.id,
name: device.name,
mac: mac,
rssi: device.rssi || -100,
wellId,
});
}
}
);
// Stop scan after timeout
setTimeout(() => {
this.stopScan();
resolve(Array.from(foundDevices.values()));
}, BLE_CONFIG.SCAN_TIMEOUT);
});
}
stopScan(): void {
if (this.scanning) {
this.manager.stopDeviceScan();
this.scanning = false;
}
}
async connectDevice(deviceId: string): Promise<boolean> {
try {
const device = await this.manager.connectToDevice(deviceId);
await device.discoverAllServicesAndCharacteristics();
this.connectedDevices.set(deviceId, device);
return true;
} catch (error) {
return false;
}
}
async disconnectDevice(deviceId: string): Promise<void> {
const device = this.connectedDevices.get(deviceId);
if (device) {
await device.cancelConnection();
this.connectedDevices.delete(deviceId);
}
}
isDeviceConnected(deviceId: string): boolean {
return this.connectedDevices.has(deviceId);
}
async sendCommand(deviceId: string, command: string): Promise<string> {
const device = this.connectedDevices.get(deviceId);
if (!device) {
throw new Error('Device not connected');
}
return new Promise(async (resolve, reject) => {
let responseReceived = false;
let response = '';
try {
// Subscribe to notifications
device.monitorCharacteristicForService(
BLE_CONFIG.SERVICE_UUID,
BLE_CONFIG.CHAR_UUID,
(error, characteristic) => {
if (error) {
if (!responseReceived) {
responseReceived = true;
reject(error);
}
return;
}
if (characteristic?.value) {
const decoded = base64.decode(characteristic.value);
response = decoded;
responseReceived = true;
resolve(decoded);
}
}
);
// Send command
const encoded = base64.encode(command);
await device.writeCharacteristicWithResponseForService(
BLE_CONFIG.SERVICE_UUID,
BLE_CONFIG.CHAR_UUID,
encoded
);
// Timeout
setTimeout(() => {
if (!responseReceived) {
responseReceived = true;
reject(new Error('Command timeout'));
}
}, BLE_CONFIG.COMMAND_TIMEOUT);
} catch (error) {
reject(error);
}
});
}
async getWiFiList(deviceId: string): Promise<WiFiNetwork[]> {
// Step 1: Unlock device
const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
if (!unlockResponse.includes('ok')) {
throw new Error('Failed to unlock device');
}
// Step 2: Get WiFi list
const listResponse = await this.sendCommand(deviceId, BLE_COMMANDS.GET_WIFI_LIST);
// Parse response: "mac,XXXXXX|w|COUNT|SSID1,RSSI1|SSID2,RSSI2|..."
const parts = listResponse.split('|');
if (parts.length < 3) {
throw new Error('Invalid WiFi list response');
}
const count = parseInt(parts[2], 10);
if (count < 0) {
if (count === -1) {
throw new Error('WiFi scan in progress, please wait');
}
if (count === -2) {
return []; // No networks found
}
}
const networks: WiFiNetwork[] = [];
for (let i = 3; i < parts.length; i++) {
const [ssid, rssiStr] = parts[i].split(',');
if (ssid && rssiStr) {
networks.push({
ssid: ssid.trim(),
rssi: parseInt(rssiStr, 10),
});
}
}
// Sort by signal strength (strongest first)
return networks.sort((a, b) => b.rssi - a.rssi);
}
async setWiFi(deviceId: string, ssid: string, password: string): Promise<boolean> {
// Step 1: Unlock device
const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
if (!unlockResponse.includes('ok')) {
throw new Error('Failed to unlock device');
}
// Step 2: Set WiFi credentials
const command = `${BLE_COMMANDS.SET_WIFI}|${ssid},${password}`;
const setResponse = await this.sendCommand(deviceId, command);
// Parse response: "mac,XXXXXX|W|ok" or "mac,XXXXXX|W|fail"
return setResponse.includes('|W|ok');
}
async getCurrentWiFi(deviceId: string): Promise<WiFiStatus | null> {
// Step 1: Unlock device
const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
if (!unlockResponse.includes('ok')) {
throw new Error('Failed to unlock device');
}
// Step 2: Get current WiFi status
const statusResponse = await this.sendCommand(deviceId, BLE_COMMANDS.GET_WIFI_STATUS);
// Parse response: "mac,XXXXXX|a|SSID,RSSI" or "mac,XXXXXX|a|,0" (not connected)
const parts = statusResponse.split('|');
if (parts.length < 3) {
return null;
}
const [ssid, rssiStr] = parts[2].split(',');
if (!ssid || ssid.trim() === '') {
return null; // Not connected
}
return {
ssid: ssid.trim(),
rssi: parseInt(rssiStr, 10),
connected: true,
};
}
async rebootDevice(deviceId: string): Promise<void> {
// Step 1: Unlock device
await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
// Step 2: Reboot (device will disconnect)
await this.sendCommand(deviceId, BLE_COMMANDS.REBOOT);
// Remove from connected devices
this.connectedDevices.delete(deviceId);
}
}