Add BLE infrastructure for sensor connectivity
Core BLE system: - BLEManager: Real BLE device scanning and connection (iOS/Android) - MockBLEManager: Simulator-safe mock for development - BLEContext: React context for BLE state management - BLEProvider: Added to app/_layout.tsx Bluetooth permissions: - iOS: NSBluetoothAlwaysUsageDescription, NSBluetoothPeripheralUsageDescription - Android: BLUETOOTH, BLUETOOTH_ADMIN, BLUETOOTH_CONNECT, BLUETOOTH_SCAN, ACCESS_FINE_LOCATION Dependencies: - react-native-ble-plx@3.5.0 - expo-device@8.0.10 - react-native-base64@0.2.2 Simulator support: - Auto-detects iOS simulator via expo-device - Falls back to MockBLEManager with fake devices - No crashes or permission errors in development
This commit is contained in:
parent
3aee73a731
commit
86e73f004d
19
app.json
19
app.json
@ -16,7 +16,9 @@
|
|||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"ITSAppUsesNonExemptEncryption": false,
|
"ITSAppUsesNonExemptEncryption": false,
|
||||||
"NSSpeechRecognitionUsageDescription": "Allow $(PRODUCT_NAME) to use speech recognition.",
|
"NSSpeechRecognitionUsageDescription": "Allow $(PRODUCT_NAME) to use speech recognition.",
|
||||||
"NSMicrophoneUsageDescription": "Allow $(PRODUCT_NAME) to use the microphone."
|
"NSMicrophoneUsageDescription": "Allow $(PRODUCT_NAME) to use the microphone.",
|
||||||
|
"NSBluetoothAlwaysUsageDescription": "WellNuo needs Bluetooth to connect to your wellness sensors and monitor their status.",
|
||||||
|
"NSBluetoothPeripheralUsageDescription": "WellNuo needs Bluetooth to manage and configure your sensors."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
@ -31,7 +33,12 @@
|
|||||||
"predictiveBackGestureEnabled": false,
|
"predictiveBackGestureEnabled": false,
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"android.permission.RECORD_AUDIO",
|
"android.permission.RECORD_AUDIO",
|
||||||
"android.permission.MODIFY_AUDIO_SETTINGS"
|
"android.permission.MODIFY_AUDIO_SETTINGS",
|
||||||
|
"android.permission.BLUETOOTH",
|
||||||
|
"android.permission.BLUETOOTH_ADMIN",
|
||||||
|
"android.permission.BLUETOOTH_CONNECT",
|
||||||
|
"android.permission.BLUETOOTH_SCAN",
|
||||||
|
"android.permission.ACCESS_FINE_LOCATION"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
@ -69,6 +76,14 @@
|
|||||||
"merchantIdentifier": "merchant.com.wellnuo.app",
|
"merchantIdentifier": "merchant.com.wellnuo.app",
|
||||||
"enableGooglePay": true
|
"enableGooglePay": true
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"react-native-ble-plx",
|
||||||
|
{
|
||||||
|
"isBackgroundEnabled": true,
|
||||||
|
"modes": ["peripheral", "central"],
|
||||||
|
"bluetoothAlwaysPermission": "Allow $(PRODUCT_NAME) to connect to WellNuo sensors"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
|||||||
import { ToastProvider } from '@/components/ui/Toast';
|
import { ToastProvider } from '@/components/ui/Toast';
|
||||||
import { AuthProvider, useAuth } from '@/contexts/AuthContext';
|
import { AuthProvider, useAuth } from '@/contexts/AuthContext';
|
||||||
import { BeneficiaryProvider } from '@/contexts/BeneficiaryContext';
|
import { BeneficiaryProvider } from '@/contexts/BeneficiaryContext';
|
||||||
|
import { BLEProvider } from '@/contexts/BLEContext';
|
||||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
import { useColorScheme } from '@/hooks/use-color-scheme';
|
||||||
|
|
||||||
// Stripe publishable key (test mode) - must match backend STRIPE_PUBLISHABLE_KEY
|
// Stripe publishable key (test mode) - must match backend STRIPE_PUBLISHABLE_KEY
|
||||||
@ -95,9 +96,11 @@ export default function RootLayout() {
|
|||||||
>
|
>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<BeneficiaryProvider>
|
<BeneficiaryProvider>
|
||||||
|
<BLEProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<RootLayoutNav />
|
<RootLayoutNav />
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
|
</BLEProvider>
|
||||||
</BeneficiaryProvider>
|
</BeneficiaryProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</StripeProvider>
|
</StripeProvider>
|
||||||
|
|||||||
171
contexts/BLEContext.tsx
Normal file
171
contexts/BLEContext.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
// BLE Context - Global state for Bluetooth management
|
||||||
|
|
||||||
|
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
||||||
|
import { bleManager, WPDevice, WiFiNetwork, WiFiStatus, isBLEAvailable } from '@/services/ble';
|
||||||
|
|
||||||
|
interface BLEContextType {
|
||||||
|
// State
|
||||||
|
foundDevices: WPDevice[];
|
||||||
|
isScanning: boolean;
|
||||||
|
connectedDevices: Set<string>;
|
||||||
|
isBLEAvailable: boolean;
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
scanDevices: () => Promise<void>;
|
||||||
|
stopScan: () => void;
|
||||||
|
connectDevice: (deviceId: string) => Promise<boolean>;
|
||||||
|
disconnectDevice: (deviceId: string) => Promise<void>;
|
||||||
|
getWiFiList: (deviceId: string) => Promise<WiFiNetwork[]>;
|
||||||
|
setWiFi: (deviceId: string, ssid: string, password: string) => Promise<boolean>;
|
||||||
|
getCurrentWiFi: (deviceId: string) => Promise<WiFiStatus | null>;
|
||||||
|
rebootDevice: (deviceId: string) => Promise<void>;
|
||||||
|
clearError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BLEContext = createContext<BLEContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function BLEProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [foundDevices, setFoundDevices] = useState<WPDevice[]>([]);
|
||||||
|
const [isScanning, setIsScanning] = useState(false);
|
||||||
|
const [connectedDevices, setConnectedDevices] = useState<Set<string>>(new Set());
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const scanDevices = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
setIsScanning(true);
|
||||||
|
const devices = await bleManager.scanDevices();
|
||||||
|
// Sort by RSSI (strongest first)
|
||||||
|
const sorted = devices.sort((a, b) => b.rssi - a.rssi);
|
||||||
|
setFoundDevices(sorted);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[BLEContext] Scan error:', err);
|
||||||
|
setError(err.message || 'Failed to scan for devices');
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setIsScanning(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const stopScan = useCallback(() => {
|
||||||
|
bleManager.stopScan();
|
||||||
|
setIsScanning(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const connectDevice = useCallback(async (deviceId: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
const success = await bleManager.connectDevice(deviceId);
|
||||||
|
if (success) {
|
||||||
|
setConnectedDevices(prev => new Set(prev).add(deviceId));
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[BLEContext] Connect error:', err);
|
||||||
|
setError(err.message || 'Failed to connect to device');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const disconnectDevice = useCallback(async (deviceId: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await bleManager.disconnectDevice(deviceId);
|
||||||
|
setConnectedDevices(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(deviceId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[BLEContext] Disconnect error:', err);
|
||||||
|
setError(err.message || 'Failed to disconnect device');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getWiFiList = useCallback(async (deviceId: string): Promise<WiFiNetwork[]> => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
return await bleManager.getWiFiList(deviceId);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[BLEContext] Get WiFi list error:', err);
|
||||||
|
setError(err.message || 'Failed to get WiFi networks');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setWiFi = useCallback(
|
||||||
|
async (deviceId: string, ssid: string, password: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
return await bleManager.setWiFi(deviceId, ssid, password);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[BLEContext] Set WiFi error:', err);
|
||||||
|
setError(err.message || 'Failed to configure WiFi');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getCurrentWiFi = useCallback(
|
||||||
|
async (deviceId: string): Promise<WiFiStatus | null> => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
return await bleManager.getCurrentWiFi(deviceId);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[BLEContext] Get current WiFi error:', err);
|
||||||
|
setError(err.message || 'Failed to get current WiFi');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const rebootDevice = useCallback(async (deviceId: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
await bleManager.rebootDevice(deviceId);
|
||||||
|
// Remove from connected devices
|
||||||
|
setConnectedDevices(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(deviceId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[BLEContext] Reboot error:', err);
|
||||||
|
setError(err.message || 'Failed to reboot device');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearError = useCallback(() => {
|
||||||
|
setError(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value: BLEContextType = {
|
||||||
|
foundDevices,
|
||||||
|
isScanning,
|
||||||
|
connectedDevices,
|
||||||
|
isBLEAvailable,
|
||||||
|
error,
|
||||||
|
scanDevices,
|
||||||
|
stopScan,
|
||||||
|
connectDevice,
|
||||||
|
disconnectDevice,
|
||||||
|
getWiFiList,
|
||||||
|
setWiFi,
|
||||||
|
getCurrentWiFi,
|
||||||
|
rebootDevice,
|
||||||
|
clearError,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <BLEContext.Provider value={value}>{children}</BLEContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBLE() {
|
||||||
|
const context = useContext(BLEContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useBLE must be used within a BLEProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
141
package-lock.json
generated
141
package-lock.json
generated
@ -15,13 +15,14 @@
|
|||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
"@stripe/stripe-react-native": "0.50.3",
|
"@stripe/stripe-react-native": "0.50.3",
|
||||||
"expo": "~54.0.30",
|
"expo": "~54.0.31",
|
||||||
"expo-audio": "~1.1.1",
|
"expo-audio": "~1.1.1",
|
||||||
"expo-av": "~16.0.8",
|
"expo-av": "~16.0.8",
|
||||||
"expo-build-properties": "~1.0.10",
|
"expo-build-properties": "~1.0.10",
|
||||||
"expo-camera": "~17.0.10",
|
"expo-camera": "~17.0.10",
|
||||||
"expo-clipboard": "~8.0.8",
|
"expo-clipboard": "~8.0.8",
|
||||||
"expo-constants": "~18.0.12",
|
"expo-constants": "~18.0.13",
|
||||||
|
"expo-device": "^8.0.10",
|
||||||
"expo-file-system": "~19.0.21",
|
"expo-file-system": "~19.0.21",
|
||||||
"expo-font": "~14.0.10",
|
"expo-font": "~14.0.10",
|
||||||
"expo-haptics": "~15.0.8",
|
"expo-haptics": "~15.0.8",
|
||||||
@ -41,9 +42,11 @@
|
|||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
|
"react-native-base64": "^0.2.2",
|
||||||
|
"react-native-ble-plx": "^3.5.0",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-get-random-values": "^2.0.0",
|
"react-native-get-random-values": "~1.11.0",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-root-toast": "^4.0.1",
|
"react-native-root-toast": "^4.0.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
@ -2474,13 +2477,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/code-signing-certificates": {
|
"node_modules/@expo/code-signing-certificates": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz",
|
||||||
"integrity": "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw==",
|
"integrity": "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-forge": "^1.2.1",
|
"node-forge": "^1.3.3"
|
||||||
"nullthrows": "^1.1.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/config": {
|
"node_modules/@expo/config": {
|
||||||
@ -2748,9 +2750,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/metro-config": {
|
"node_modules/@expo/metro-config": {
|
||||||
"version": "54.0.12",
|
"version": "54.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.13.tgz",
|
||||||
"integrity": "sha512-Xhv1z/ak/cuJWeLxlnWr2u22q2AM/klASbjpP5eE34y91lGWa2NUwrFWoS830MhJ6kuAqtGdoQhwyPa3TES7sA==",
|
"integrity": "sha512-RRufMCgLR2Za1WGsh02OatIJo5qZFt31yCnIOSfoubNc3Qqe92Z41pVsbrFnmw5CIaisv1NgdBy05DHe7pEyuw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.20.0",
|
"@babel/code-frame": "^7.20.0",
|
||||||
@ -10553,28 +10555,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo": {
|
"node_modules/expo": {
|
||||||
"version": "54.0.30",
|
"version": "54.0.31",
|
||||||
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.30.tgz",
|
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.31.tgz",
|
||||||
"integrity": "sha512-6q+aFfKL0SpT8prfdpR3V8HcN51ov0mCGuwQTzyuk6eeO9rg7a7LWbgPv9rEVXGZEuyULstL8LGNwHqusand7Q==",
|
"integrity": "sha512-kQ3RDqA/a59I7y+oqQGyrPbbYlgPMUdKBOgvFLpoHbD2bCM+F75i4N0mUijy7dG5F/CUCu2qHmGGUCXBbMDkCg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.0",
|
"@babel/runtime": "^7.20.0",
|
||||||
"@expo/cli": "54.0.20",
|
"@expo/cli": "54.0.21",
|
||||||
"@expo/config": "~12.0.13",
|
"@expo/config": "~12.0.13",
|
||||||
"@expo/config-plugins": "~54.0.4",
|
"@expo/config-plugins": "~54.0.4",
|
||||||
"@expo/devtools": "0.1.8",
|
"@expo/devtools": "0.1.8",
|
||||||
"@expo/fingerprint": "0.15.4",
|
"@expo/fingerprint": "0.15.4",
|
||||||
"@expo/metro": "~54.2.0",
|
"@expo/metro": "~54.2.0",
|
||||||
"@expo/metro-config": "54.0.12",
|
"@expo/metro-config": "54.0.13",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@ungap/structured-clone": "^1.3.0",
|
"@ungap/structured-clone": "^1.3.0",
|
||||||
"babel-preset-expo": "~54.0.9",
|
"babel-preset-expo": "~54.0.9",
|
||||||
"expo-asset": "~12.0.12",
|
"expo-asset": "~12.0.12",
|
||||||
"expo-constants": "~18.0.12",
|
"expo-constants": "~18.0.13",
|
||||||
"expo-file-system": "~19.0.21",
|
"expo-file-system": "~19.0.21",
|
||||||
"expo-font": "~14.0.10",
|
"expo-font": "~14.0.10",
|
||||||
"expo-keep-awake": "~15.0.8",
|
"expo-keep-awake": "~15.0.8",
|
||||||
"expo-modules-autolinking": "3.0.23",
|
"expo-modules-autolinking": "3.0.24",
|
||||||
"expo-modules-core": "3.0.29",
|
"expo-modules-core": "3.0.29",
|
||||||
"pretty-format": "^29.7.0",
|
"pretty-format": "^29.7.0",
|
||||||
"react-refresh": "^0.14.2",
|
"react-refresh": "^0.14.2",
|
||||||
@ -10727,12 +10729,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-constants": {
|
"node_modules/expo-constants": {
|
||||||
"version": "18.0.12",
|
"version": "18.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz",
|
||||||
"integrity": "sha512-WzcKYMVNRRu4NcSzfIVRD5aUQFnSpTZgXFrlWmm19xJoDa4S3/PQNi6PNTBRc49xz9h8FT7HMxRKaC8lr0gflA==",
|
"integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/config": "~12.0.12",
|
"@expo/config": "~12.0.13",
|
||||||
"@expo/env": "~2.0.8"
|
"@expo/env": "~2.0.8"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@ -10740,6 +10742,44 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-device": {
|
||||||
|
"version": "8.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-device/-/expo-device-8.0.10.tgz",
|
||||||
|
"integrity": "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ua-parser-js": "^0.7.33"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-device/node_modules/ua-parser-js": {
|
||||||
|
"version": "0.7.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz",
|
||||||
|
"integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ua-parser-js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/faisalman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/faisalman"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"ua-parser-js": "script/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-file-system": {
|
"node_modules/expo-file-system": {
|
||||||
"version": "19.0.21",
|
"version": "19.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
|
||||||
@ -10848,9 +10888,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-modules-autolinking": {
|
"node_modules/expo-modules-autolinking": {
|
||||||
"version": "3.0.23",
|
"version": "3.0.24",
|
||||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.23.tgz",
|
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz",
|
||||||
"integrity": "sha512-YZnaE0G+52xftjH5nsIRaWsoVBY38SQCECclpdgLisdbRY/6Mzo7ndokjauOv3mpFmzMZACHyJNu1YSAffQwTg==",
|
"integrity": "sha512-TP+6HTwhL7orDvsz2VzauyQlXJcAWyU3ANsZ7JGL4DQu8XaZv/A41ZchbtAYLfozNA2Ya1Hzmhx65hXryBMjaQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/spawn-async": "^1.7.2",
|
"@expo/spawn-async": "^1.7.2",
|
||||||
@ -11236,13 +11276,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo/node_modules/@expo/cli": {
|
"node_modules/expo/node_modules/@expo/cli": {
|
||||||
"version": "54.0.20",
|
"version": "54.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.20.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.21.tgz",
|
||||||
"integrity": "sha512-cwsXmhftvS0p9NNYOhXGnicBAZl9puWwRt19Qq5eQ6njLnaj8WvcR+kDZyADtgZxBsZiyVlrKXvnjt43HXywQA==",
|
"integrity": "sha512-L/FdpyZDsg/Nq6xW6kfiyF9DUzKfLZCKFXEVZcDqCNar6bXxQVotQyvgexRvtUF5nLinuT/UafLOdC3FUALUmA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@0no-co/graphql.web": "^1.0.8",
|
"@0no-co/graphql.web": "^1.0.8",
|
||||||
"@expo/code-signing-certificates": "^0.0.5",
|
"@expo/code-signing-certificates": "^0.0.6",
|
||||||
"@expo/config": "~12.0.13",
|
"@expo/config": "~12.0.13",
|
||||||
"@expo/config-plugins": "~54.0.4",
|
"@expo/config-plugins": "~54.0.4",
|
||||||
"@expo/devcert": "^1.2.1",
|
"@expo/devcert": "^1.2.1",
|
||||||
@ -11250,7 +11290,7 @@
|
|||||||
"@expo/image-utils": "^0.8.8",
|
"@expo/image-utils": "^0.8.8",
|
||||||
"@expo/json-file": "^10.0.8",
|
"@expo/json-file": "^10.0.8",
|
||||||
"@expo/metro": "~54.2.0",
|
"@expo/metro": "~54.2.0",
|
||||||
"@expo/metro-config": "~54.0.12",
|
"@expo/metro-config": "~54.0.13",
|
||||||
"@expo/osascript": "^2.3.8",
|
"@expo/osascript": "^2.3.8",
|
||||||
"@expo/package-manager": "^1.9.9",
|
"@expo/package-manager": "^1.9.9",
|
||||||
"@expo/plist": "^0.4.8",
|
"@expo/plist": "^0.4.8",
|
||||||
@ -11279,7 +11319,7 @@
|
|||||||
"glob": "^13.0.0",
|
"glob": "^13.0.0",
|
||||||
"lan-network": "^0.1.6",
|
"lan-network": "^0.1.6",
|
||||||
"minimatch": "^9.0.0",
|
"minimatch": "^9.0.0",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.3",
|
||||||
"npm-package-arg": "^11.0.0",
|
"npm-package-arg": "^11.0.0",
|
||||||
"ora": "^3.4.0",
|
"ora": "^3.4.0",
|
||||||
"picomatch": "^3.0.1",
|
"picomatch": "^3.0.1",
|
||||||
@ -11386,9 +11426,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo/node_modules/ws": {
|
"node_modules/expo/node_modules/ws": {
|
||||||
"version": "8.18.3",
|
"version": "8.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
@ -18790,6 +18830,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-base64": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-base64/-/react-native-base64-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-9iDzlDQrJqRlgoi7GnO4dqK/7/6lpA3DFrArhp85tDB7ZI6wLr7luHihb/pX6jhm4zlHqOz2OYSGJ6PSgyUO1g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/react-native-ble-plx": {
|
||||||
|
"version": "3.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-ble-plx/-/react-native-ble-plx-3.5.0.tgz",
|
||||||
|
"integrity": "sha512-PeSnRswHLwLRVMQkOfDaRICtrGmo94WGKhlSC09XmHlqX2EuYgH+vNJpGcLkd8lyiYpEJyf8wlFAdj9Akliwmw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-fs": {
|
"node_modules/react-native-fs": {
|
||||||
"version": "2.20.0",
|
"version": "2.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz",
|
||||||
@ -18825,15 +18884,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-native-get-random-values": {
|
"node_modules/react-native-get-random-values": {
|
||||||
"version": "2.0.0",
|
"version": "1.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz",
|
||||||
"integrity": "sha512-wx7/aPqsUIiWsG35D+MsUJd8ij96e3JKddklSdrdZUrheTx89gPtz3Q2yl9knBArj5u26Cl23T88ai+Q0vypdQ==",
|
"integrity": "sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-base64-decode": "^1.0.0"
|
"fast-base64-decode": "^1.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react-native": ">=0.81"
|
"react-native": ">=0.56"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-native-is-edge-to-edge": {
|
"node_modules/react-native-is-edge-to-edge": {
|
||||||
@ -24327,9 +24386,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.22.0",
|
"version": "6.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||||
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
|
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"node": ">=18.17"
|
||||||
|
|||||||
@ -18,13 +18,14 @@
|
|||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
"@stripe/stripe-react-native": "0.50.3",
|
"@stripe/stripe-react-native": "0.50.3",
|
||||||
"expo": "~54.0.30",
|
"expo": "~54.0.31",
|
||||||
"expo-audio": "~1.1.1",
|
"expo-audio": "~1.1.1",
|
||||||
"expo-av": "~16.0.8",
|
"expo-av": "~16.0.8",
|
||||||
"expo-build-properties": "~1.0.10",
|
"expo-build-properties": "~1.0.10",
|
||||||
"expo-camera": "~17.0.10",
|
"expo-camera": "~17.0.10",
|
||||||
"expo-clipboard": "~8.0.8",
|
"expo-clipboard": "~8.0.8",
|
||||||
"expo-constants": "~18.0.12",
|
"expo-constants": "~18.0.13",
|
||||||
|
"expo-device": "^8.0.10",
|
||||||
"expo-file-system": "~19.0.21",
|
"expo-file-system": "~19.0.21",
|
||||||
"expo-font": "~14.0.10",
|
"expo-font": "~14.0.10",
|
||||||
"expo-haptics": "~15.0.8",
|
"expo-haptics": "~15.0.8",
|
||||||
@ -44,9 +45,11 @@
|
|||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
|
"react-native-base64": "^0.2.2",
|
||||||
|
"react-native-ble-plx": "^3.5.0",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-get-random-values": "^2.0.0",
|
"react-native-get-random-values": "~1.11.0",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-root-toast": "^4.0.1",
|
"react-native-root-toast": "^4.0.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
|
|||||||
287
services/ble/BLEManager.ts
Normal file
287
services/ble/BLEManager.ts
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
// 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) {
|
||||||
|
console.error('[BLE] Connection failed:', 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
services/ble/MockBLEManager.ts
Normal file
112
services/ble/MockBLEManager.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// Mock BLE Manager для iOS Simulator (Bluetooth недоступен)
|
||||||
|
|
||||||
|
import { IBLEManager, WPDevice, WiFiNetwork, WiFiStatus } from './types';
|
||||||
|
|
||||||
|
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
export class MockBLEManager implements IBLEManager {
|
||||||
|
private connectedDevices = new Set<string>();
|
||||||
|
private mockDevices: WPDevice[] = [
|
||||||
|
{
|
||||||
|
id: 'mock-743',
|
||||||
|
name: 'WP_497_81a14c',
|
||||||
|
mac: '142B2F81A14C',
|
||||||
|
rssi: -55,
|
||||||
|
wellId: 497,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mock-769',
|
||||||
|
name: 'WP_523_81aad4',
|
||||||
|
mac: '142B2F81AAD4',
|
||||||
|
rssi: -67,
|
||||||
|
wellId: 523,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async scanDevices(): Promise<WPDevice[]> {
|
||||||
|
console.log('[MockBLE] Scanning for devices...');
|
||||||
|
await delay(2000); // Simulate scan delay
|
||||||
|
return this.mockDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopScan(): void {
|
||||||
|
console.log('[MockBLE] Scan stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectDevice(deviceId: string): Promise<boolean> {
|
||||||
|
console.log(`[MockBLE] Connecting to ${deviceId}...`);
|
||||||
|
await delay(1000);
|
||||||
|
this.connectedDevices.add(deviceId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnectDevice(deviceId: string): Promise<void> {
|
||||||
|
console.log(`[MockBLE] Disconnecting ${deviceId}`);
|
||||||
|
await delay(500);
|
||||||
|
this.connectedDevices.delete(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
isDeviceConnected(deviceId: string): boolean {
|
||||||
|
return this.connectedDevices.has(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendCommand(deviceId: string, command: string): Promise<string> {
|
||||||
|
console.log(`[MockBLE] Sending command: ${command}`);
|
||||||
|
await delay(500);
|
||||||
|
|
||||||
|
// Simulate responses
|
||||||
|
if (command === 'pin|7856') {
|
||||||
|
return 'pin|ok';
|
||||||
|
}
|
||||||
|
if (command === 'w') {
|
||||||
|
return 'mac,142b2f81a14c|w|3|FrontierTower,-55|HomeNetwork,-67|TP-Link_5G,-75';
|
||||||
|
}
|
||||||
|
if (command === 'a') {
|
||||||
|
return 'mac,142b2f81a14c|a|FrontierTower,-67';
|
||||||
|
}
|
||||||
|
if (command.startsWith('W|')) {
|
||||||
|
return 'mac,142b2f81a14c|W|ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWiFiList(deviceId: string): Promise<WiFiNetwork[]> {
|
||||||
|
console.log(`[MockBLE] Getting WiFi list for ${deviceId}`);
|
||||||
|
await delay(1500);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ ssid: 'FrontierTower', rssi: -55 },
|
||||||
|
{ ssid: 'HomeNetwork', rssi: -67 },
|
||||||
|
{ ssid: 'TP-Link_5G', rssi: -75 },
|
||||||
|
{ ssid: 'Office-WiFi', rssi: -80 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async setWiFi(
|
||||||
|
deviceId: string,
|
||||||
|
ssid: string,
|
||||||
|
password: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
console.log(`[MockBLE] Setting WiFi: ${ssid}`);
|
||||||
|
await delay(2000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentWiFi(deviceId: string): Promise<WiFiStatus | null> {
|
||||||
|
console.log(`[MockBLE] Getting current WiFi for ${deviceId}`);
|
||||||
|
await delay(1000);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ssid: 'FrontierTower',
|
||||||
|
rssi: -67,
|
||||||
|
connected: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async rebootDevice(deviceId: string): Promise<void> {
|
||||||
|
console.log(`[MockBLE] Rebooting ${deviceId}`);
|
||||||
|
await delay(500);
|
||||||
|
this.connectedDevices.delete(deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
services/ble/index.ts
Normal file
17
services/ble/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// BLE Service entry point
|
||||||
|
|
||||||
|
import * as Device from 'expo-device';
|
||||||
|
import { RealBLEManager } from './BLEManager';
|
||||||
|
import { MockBLEManager } from './MockBLEManager';
|
||||||
|
import { IBLEManager } from './types';
|
||||||
|
|
||||||
|
// Determine if BLE is available (real device vs simulator)
|
||||||
|
export const isBLEAvailable = Device.isDevice;
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const bleManager: IBLEManager = isBLEAvailable
|
||||||
|
? new RealBLEManager()
|
||||||
|
: new MockBLEManager();
|
||||||
|
|
||||||
|
// Re-export types
|
||||||
|
export * from './types';
|
||||||
60
services/ble/types.ts
Normal file
60
services/ble/types.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// BLE Service Types
|
||||||
|
|
||||||
|
export interface WPDevice {
|
||||||
|
id: string; // BLE device ID
|
||||||
|
name: string; // "WP_497_81a14c"
|
||||||
|
mac: string; // "142B2F81A14C"
|
||||||
|
rssi: number; // Signal strength in dBm (-55, -67, etc.)
|
||||||
|
wellId?: number; // Parsed from name (497, 523)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WiFiNetwork {
|
||||||
|
ssid: string;
|
||||||
|
rssi: number; // Signal strength in dBm
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WiFiStatus {
|
||||||
|
ssid: string;
|
||||||
|
rssi: number;
|
||||||
|
connected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BLECommand {
|
||||||
|
PIN_UNLOCK: 'pin|7856';
|
||||||
|
GET_WIFI_LIST: 'w';
|
||||||
|
SET_WIFI: 'W'; // Format: W|SSID,PASSWORD
|
||||||
|
GET_WIFI_STATUS: 'a';
|
||||||
|
REBOOT: 's';
|
||||||
|
DISCONNECT: 'D';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BLE_COMMANDS: BLECommand = {
|
||||||
|
PIN_UNLOCK: 'pin|7856',
|
||||||
|
GET_WIFI_LIST: 'w',
|
||||||
|
SET_WIFI: 'W',
|
||||||
|
GET_WIFI_STATUS: 'a',
|
||||||
|
REBOOT: 's',
|
||||||
|
DISCONNECT: 'D',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BLE_CONFIG = {
|
||||||
|
SERVICE_UUID: '4fafc201-1fb5-459e-8fcc-c5c9c331914b',
|
||||||
|
CHAR_UUID: 'beb5483e-36e1-4688-b7f5-ea07361b26a8',
|
||||||
|
SCAN_TIMEOUT: 10000, // 10 seconds
|
||||||
|
COMMAND_TIMEOUT: 5000, // 5 seconds
|
||||||
|
DEVICE_NAME_PREFIX: 'WP_',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Interface для BLE Manager (и real и mock)
|
||||||
|
export interface IBLEManager {
|
||||||
|
scanDevices(): Promise<WPDevice[]>;
|
||||||
|
stopScan(): void;
|
||||||
|
connectDevice(deviceId: string): Promise<boolean>;
|
||||||
|
disconnectDevice(deviceId: string): Promise<void>;
|
||||||
|
isDeviceConnected(deviceId: string): boolean;
|
||||||
|
sendCommand(deviceId: string, command: string): Promise<string>;
|
||||||
|
getWiFiList(deviceId: string): Promise<WiFiNetwork[]>;
|
||||||
|
setWiFi(deviceId: string, ssid: string, password: string): Promise<boolean>;
|
||||||
|
getCurrentWiFi(deviceId: string): Promise<WiFiStatus | null>;
|
||||||
|
rebootDevice(deviceId: string): Promise<void>;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user