diff --git a/ios/WildlifeWatcher.xcodeproj/project.pbxproj b/ios/WildlifeWatcher.xcodeproj/project.pbxproj index 893fbb3..c777153 100644 --- a/ios/WildlifeWatcher.xcodeproj/project.pbxproj +++ b/ios/WildlifeWatcher.xcodeproj/project.pbxproj @@ -313,8 +313,13 @@ PRODUCT_BUNDLE_IDENTIFIER = com.wildlife.wildlifewatcher; PRODUCT_NAME = WildlifeWatcher; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -347,7 +352,12 @@ PRODUCT_NAME = WildlifeWatcher; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Wildlife Watcher App Store"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/ios/WildlifeWatcher/Info.plist b/ios/WildlifeWatcher/Info.plist index 5b51e3f..2c4e5b3 100644 --- a/ios/WildlifeWatcher/Info.plist +++ b/ios/WildlifeWatcher/Info.plist @@ -65,11 +65,17 @@ armv7 + UIStatusBarStyle + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait UIViewControllerBasedStatusBarAppearance diff --git a/src/ble/types.ts b/src/ble/types.ts index f24ff08..11571df 100644 --- a/src/ble/types.ts +++ b/src/ble/types.ts @@ -68,7 +68,6 @@ export const COMMANDS: { [CommandNames.ID]: { name: CommandNames.ID, readCommand: "id", - readRegex: /\bDevEui:\s*([0-9A-Fa-f:]+)\b/, }, [CommandNames.VERSION]: { name: CommandNames.VERSION, @@ -77,7 +76,7 @@ export const COMMANDS: { [CommandNames.BATTERY]: { name: CommandNames.BATTERY, readCommand: "battery", - readRegex: /\bBattery\s=\s([0-9.]+V)\b/, + readRegex: /\bBattery\s=\s(100|\d{1,3})%/, }, [CommandNames.SENSOR]: { name: CommandNames.SENSOR, @@ -146,3 +145,61 @@ export const COMMANDS: { readRegex: /(Device will enter DFU mode after disconnecting.)\s*/, }, } + +type CharacteristicProperty = + | "Read" + | "Write" + | "WriteWithoutResponse" + | "Notify" + +type CharacteristicProperties = { + [key in CharacteristicProperty]?: CharacteristicProperty +} + +type Descriptor = { + value: any + uuid: string +} + +type Characteristic = { + properties: CharacteristicProperties + characteristic: string + service: string + descriptors?: Descriptor[] +} + +type Service = { + uuid: string +} + +type ManufacturerRawData = { + bytes: number[] + data: string + CDVType: string +} + +type RawData = { + bytes: number[] + data: string + CDVType: string +} + +type Advertising = { + manufacturerData: any + txPowerLevel: number + isConnectable: boolean + serviceData: any + localName: string + serviceUUIDs: string[] + manufacturerRawData: ManufacturerRawData + rawData: RawData +} + +export type Services = { + characteristics: Characteristic[] + services: Service[] + advertising: Advertising + name: string + rssi: number + id: string +} diff --git a/src/components/AppDrawer.tsx b/src/components/AppDrawer.tsx index 2b07230..fff6468 100644 --- a/src/components/AppDrawer.tsx +++ b/src/components/AppDrawer.tsx @@ -24,7 +24,7 @@ export const useAppDrawer = () => useContext(DrawerContext) export const AppDrawer = ({ children }: PropsWithChildren) => { const [isOpen, setIsOpen] = useState(false) - const { padding, spacing } = useExtendedTheme() + const { appPadding, spacing } = useExtendedTheme() const { setIsLoggedIn, isLoggedIn } = useAuth() const { top } = useSafeAreaInsets() @@ -43,7 +43,10 @@ export const AppDrawer = ({ children }: PropsWithChildren) => { renderDrawerContent={() => { return ( I'm empty at the moment. diff --git a/src/components/CustomKeyboardAvoidingView.tsx b/src/components/CustomKeyboardAvoidingView.tsx index 2c8e438..a9794ea 100644 --- a/src/components/CustomKeyboardAvoidingView.tsx +++ b/src/components/CustomKeyboardAvoidingView.tsx @@ -9,6 +9,7 @@ import { } from "react-native" import { useSafeAreaInsets } from "react-native-safe-area-context" +import { useExtendedTheme } from "../theme" type Props = { style?: StyleProp @@ -22,10 +23,11 @@ export const CustomKeyboardAvoidingView = ({ }: PropsWithChildren) => { const insets = useSafeAreaInsets() const [bottomPadding, setBottomPadding] = useState(insets.bottom) + const { spacing } = useExtendedTheme() useEffect(() => { - setBottomPadding(insets.bottom) - }, [insets.bottom, insets.top]) + setBottomPadding(insets.bottom + spacing) + }, [insets.bottom, insets.top, spacing]) return ( ) => { - const { padding } = useExtendedTheme() + const { appPadding } = useExtendedTheme() return ( - - + + {children} diff --git a/src/components/ui/WWScrollView.tsx b/src/components/ui/WWScrollView.tsx new file mode 100644 index 0000000..6eb67d1 --- /dev/null +++ b/src/components/ui/WWScrollView.tsx @@ -0,0 +1,31 @@ +import { forwardRef } from "react" +import { + ScrollViewProps, + StyleProp, + StyleSheet, + View, + ViewStyle, +} from "react-native" +import { ScrollView } from "react-native-gesture-handler" + +type Props = ScrollViewProps & { + containerStyle?: StyleProp +} + +export const WWScrollView = forwardRef( + ({ children, containerStyle, ...props }, ref) => { + return ( + + true} style={containerStyle}> + {children} + + + ) + }, +) + +const styles = StyleSheet.create({ + view: { + flex: 1, + }, +}) diff --git a/src/hooks/useBle.ts b/src/hooks/useBle.ts index e8ba71c..2c07f3e 100644 --- a/src/hooks/useBle.ts +++ b/src/hooks/useBle.ts @@ -1,15 +1,16 @@ -import React, { useCallback, useEffect, useRef } from "react" +import React, { useCallback, useEffect, useRef, useState } from "react" import { Platform } from "react-native" import BleManager from "react-native-ble-manager" import { Peripheral } from "react-native-ble-manager" +import { BLE_SERVICE_UUID } from "../utils/constants" import { - BLE_CHARACTERISTIC_READ_UUID, - BLE_SERVICE_UUID, -} from "../utils/constants" -import { invokeWithTimeout, sleep } from "../utils/helpers" + extractServiceAndCharacteristic, + invokeWithTimeout, + sleep, +} from "../utils/helpers" import { guard, log, logError } from "../utils/logger" import { useAppDispatch, useAppSelector } from "../redux" import { @@ -30,6 +31,7 @@ import { CommandConstructOptions, CommandControlTypes, CommandNames, + Services, } from "../ble/types" import { constructCommandString } from "../ble/parser" @@ -73,6 +75,8 @@ const PING_REQUEST: string[] = [] export const useBle = (): ReturnType => { const { initialized } = useAppSelector((state) => state.bleLibrary) + const [isBleConnecting, setIsBleConnecting] = useState(false) + const devices = useAppSelector((state) => state.devices) const scanning = useAppSelector((state) => state.scanning) @@ -232,9 +236,7 @@ export const useBle = (): ReturnType => { ) const isDeviceReconnecting = useRef<{ [x: string]: boolean }>({}) - const isBleConnecting = Object.values(isDeviceReconnecting.current).find( - (isConnected) => isConnected, - ) + const connectDevice = useCallback( async (peripheral: ExtendedPeripheral, timeout?: number) => { if (!initialized || peripheral.loading) return peripheral @@ -261,6 +263,8 @@ export const useBle = (): ReturnType => { isDeviceReconnecting.current[peripheral.id] = true + setIsBleConnecting(true) + const newPeripheral = { ...peripheral } dispatch(deviceLoading({ id: newPeripheral.id, loading: true })) @@ -286,18 +290,22 @@ export const useBle = (): ReturnType => { log(`Device ${deviceIdentification} connected`) - await invokeWithTimeout( + const services = (await invokeWithTimeout( () => BleManager.retrieveServices(newPeripheral.id), "BleManager.retrieveServices", timeout, - ) + )) as Services log(`Device ${deviceIdentification} services retireved`) + const extractedServices = extractServiceAndCharacteristic(services) + + newPeripheral.services = extractedServices + await BleManager.startNotification( newPeripheral.id, - BLE_SERVICE_UUID, - BLE_CHARACTERISTIC_READ_UUID, + extractedServices.serviceCharacteristic, + extractedServices.readCharacteristic, ) log(`Device ${deviceIdentification} notifications started`) @@ -313,7 +321,6 @@ export const useBle = (): ReturnType => { await ping() newPeripheral.connected = true - newPeripheral.intervals = { ping: setInterval(async () => await ping(), 20000), } @@ -329,6 +336,7 @@ export const useBle = (): ReturnType => { dispatch(deviceLoading({ id: newPeripheral.id, loading: false })) if (isDeviceReconnecting.current[peripheral.id]) { + setIsBleConnecting(false) isDeviceReconnecting.current[peripheral.id] = false } diff --git a/src/hooks/useBleListeners.tsx b/src/hooks/useBleListeners.tsx index 44f0371..6640921 100644 --- a/src/hooks/useBleListeners.tsx +++ b/src/hooks/useBleListeners.tsx @@ -186,7 +186,6 @@ export const useBleListeners = () => { console.debug(JSON.stringify(text)) const currentConfiguration = configRef.current[peripheral] || {} - // const currentPeripheral = devicesRef.current[peripheral] const currentLog = allLogs.current[peripheral] || "" if (allLogs.current[peripheral]) { diff --git a/src/hooks/useCommand.tsx b/src/hooks/useCommand.tsx index 9e2e549..46568f6 100644 --- a/src/hooks/useCommand.tsx +++ b/src/hooks/useCommand.tsx @@ -26,7 +26,9 @@ const TIMEOUT = 1000 * 10 export const useCommand = ({ deviceId, command }: Props) => { const requestRef = useRef() const timeoutRef = useRef() + const [goal, setGoal] = useState() + const [commandLoading, setCommandLoading] = useState(true) const { write } = useBleActions() const devices = useAppSelector((state) => state.devices) @@ -71,6 +73,7 @@ export const useCommand = ({ deviceId, command }: Props) => { const set = useCallback( (data?: string) => { clearTimers() + setCommandLoading(true) sendCommand(CommandControlTypes.WRITE, data) @@ -86,6 +89,8 @@ export const useCommand = ({ deviceId, command }: Props) => { clearInterval(requestRef.current) } + setCommandLoading(false) + dispatch( deviceConfigChanged({ id: deviceId, @@ -122,6 +127,8 @@ export const useCommand = ({ deviceId, command }: Props) => { // Means its a set only command in reality if (!command.readCommand) return + setCommandLoading(true) + sendCommand(CommandControlTypes.READ) requestRef.current = setInterval( @@ -133,6 +140,8 @@ export const useCommand = ({ deviceId, command }: Props) => { clearInterval(requestRef.current) } + setCommandLoading(false) + dispatch( deviceConfigChanged({ id: deviceId, @@ -170,7 +179,7 @@ export const useCommand = ({ deviceId, command }: Props) => { }) ) { clearTimers() - + setCommandLoading(false) /** * If the hook already detects the correct configuration * when it first renders, the get() method isn't even @@ -209,6 +218,7 @@ export const useCommand = ({ deviceId, command }: Props) => { return { set, get, + commandLoading, } } @@ -221,6 +231,8 @@ const isCommandCompleted = ({ }) => { if (!config) return false + if (config.loading) return false + if (goal) { return config.value === goal } diff --git a/src/navigation/screens/TerminalScreen.tsx b/src/navigation/screens/TerminalScreen.tsx index 815bfd7..8dab494 100644 --- a/src/navigation/screens/TerminalScreen.tsx +++ b/src/navigation/screens/TerminalScreen.tsx @@ -7,7 +7,6 @@ import { useEffect } from "react" import { NativeScrollEvent, NativeSyntheticEvent, - ScrollView, StyleSheet, View, } from "react-native" @@ -19,13 +18,18 @@ import { useCommand } from "../../hooks/useCommand" import { COMMANDS } from "../../ble/types" import { useSelectDevice } from "../../hooks/useSelectDevice" import { - ActivityIndicator, Button, + Divider, IconButton, + Switch, TextInput, - useTheme, } from "react-native-paper" import { WWText } from "../../components/ui/WWText" +import { useExtendedTheme } from "../../theme" +import { WWTextInput } from "../../components/ui/WWTextInput" +import { WWScreenView } from "../../components/ui/WWScreenView" +import { WWScrollView } from "../../components/ui/WWScrollView" +import { AppLoading } from "./AppLoading" type Props = { embed?: boolean @@ -33,7 +37,6 @@ type Props = { export const Terminal = ({ embed }: Props) => { const scrollViewRef = React.useRef() - const { colors } = useTheme() const { params: { deviceId }, } = useRoute>() @@ -46,13 +49,31 @@ export const Terminal = ({ embed }: Props) => { const logs = deviceLogs[deviceId] const configuration = useAppSelector((state) => state.configuration) const config = configuration[deviceId] - - useCommand({ deviceId, command: COMMANDS.BATTERY }) - useCommand({ deviceId, command: COMMANDS.VERSION }) - const { set: setHb } = useCommand({ deviceId, command: COMMANDS.HEARTBEAT }) - const { set: setAppEui } = useCommand({ deviceId, command: COMMANDS.APPEUI }) - const { set: setDevEui } = useCommand({ deviceId, command: COMMANDS.DEVEUI }) - const { set: setSensor } = useCommand({ deviceId, command: COMMANDS.SENSOR }) + const { spacing, colors, appPadding } = useExtendedTheme() + const { get: getBattery, commandLoading: batteryLoading } = useCommand({ + deviceId, + command: COMMANDS.BATTERY, + }) + const { get: getVersion, commandLoading: versionLoading } = useCommand({ + deviceId, + command: COMMANDS.VERSION, + }) + const { set: setHb, commandLoading: hbLoading } = useCommand({ + deviceId, + command: COMMANDS.HEARTBEAT, + }) + const { set: setAE, commandLoading: aeLoading } = useCommand({ + deviceId, + command: COMMANDS.APPEUI, + }) + const { set: setDE, commandLoading: deLoading } = useCommand({ + deviceId, + command: COMMANDS.DEVEUI, + }) + const { set: setSensor, commandLoading: sensorLoading } = useCommand({ + deviceId, + command: COMMANDS.SENSOR, + }) const { set: reset } = useCommand({ deviceId, command: COMMANDS.RESET }) const { set: erase } = useCommand({ deviceId, command: COMMANDS.ERASE }) const { set: triggerDfu } = useCommand({ @@ -62,6 +83,40 @@ export const Terminal = ({ embed }: Props) => { const [autoscroll, setAutoscroll] = useState(true) + const [heartbeat, setHeartbeat] = useState() + const [appEui, setAppEui] = useState("") + const [devEui, setDevEui] = useState("") + const [localSensor, setLocalSensor] = useState() + + useEffect(() => { + setLocalSensor(config.SENSOR?.value === "enable") + }, [config.SENSOR?.value]) + + const triggerSensor = (value: boolean) => { + if (!sensorLoading) { + setSensor(value ? "enable" : "disable") + setLocalSensor(value) + } + } + + const triggerHeartbeat = () => { + if (heartbeat && !hbLoading) { + setHb(`${heartbeat}s`) + } + } + + const triggerAppEui = () => { + if (appEui && appEui.length > 0 && !aeLoading) { + setAE(appEui) + } + } + + const triggerDevEui = () => { + if (devEui && devEui.length > 0 && !deLoading) { + setDE(devEui) + } + } + const writeText = useCallback(async () => { await write(device, [text]) setText("") @@ -124,179 +179,285 @@ export const Terminal = ({ embed }: Props) => { } } - const { HEARTBEAT, APPEUI, SENSOR, LORAWAN } = config + const { HEARTBEAT, APPEUI, SENSOR, LORAWAN, DEVEUI, BATTERY, VERSION, ID } = + config if ( !HEARTBEAT?.loaded || !APPEUI?.loaded || !SENSOR?.loaded || - !LORAWAN?.loaded + !LORAWAN?.loaded || + !DEVEUI?.loaded || + !BATTERY?.loaded || + !VERSION?.loaded || + !ID?.loaded ) { - return + return } - const hb = HEARTBEAT.value - const eui = APPEUI.value - const sensor = SENSOR.value - const lorawan = LORAWAN.value - return ( - - - - {logs} - - - {!autoscroll && ( - { - toggleAutoscroll(true) - scrollViewRef.current && - scrollViewRef.current.scrollToEnd({ animated: true }) - }} - /> - )} - - - setText(value)} - /> - - - - - - - - - - - - - - - - - - + + + + + + {logs.replaceAll("\r", "")} + + + {!autoscroll && ( + { + toggleAutoscroll(true) + scrollViewRef.current && + scrollViewRef.current.scrollToEnd({ animated: true }) + }} + /> + )} - - - - - - {config.HEARTBEAT && config.HEARTBEAT.loaded && ( - Current heartbeat: {hb} - )} - + + setText(value)} + right={ + + } + /> - - - + + + + + + ID and Version + + + + {ID.loaded && ( + + ID: {ID.value} + + )} + + + {VERSION.loaded && ( + + Version:{" "} + {versionLoading ? ( + "Loading..." + ) : ( + {VERSION.value} + )} + + )} + + + + + - - {config.APPEUI && config.APPEUI.loaded && ( - Current APPEUI: {eui} - )} + + Battery + + + + {BATTERY.loaded && ( + + Current battery level:{" "} + {batteryLoading ? ( + "Loading..." + ) : ( + {BATTERY.value}% + )} + + )} + + + + + - - - - - Should set APPEUI to AAA4567890123. (doesn't work) - + + Actions + + + + + + + + + + + + + + + - - - - + + Heartbeat + + + + {HEARTBEAT.loaded && ( + + Current heartbeat:{" "} + {hbLoading ? ( + "Loading..." + ) : ( + {HEARTBEAT.value} + )} + + )} + + + + setHeartbeat(value)} + style={{ marginRight: spacing }} + placeholder={HEARTBEAT.value} + /> + + - - {config.DEVEUI && config.DEVEUI.loaded && ( - Current DEVEUI: {eui} - )} + + App EUI + + + + {APPEUI.loaded && ( + + Current App EUI:{" "} + {aeLoading ? ( + "Loading..." + ) : ( + {APPEUI.value} + )} + + )} + + + + setAppEui(value)} + style={{ marginRight: spacing }} + placeholder={APPEUI.value} + /> + + - - - - - Should set DEVEUI to BBB4567890123. (doesn't work) - + + Dev EUI + + + + {DEVEUI.loaded && ( + + Current Dev EUI:{" "} + {deLoading ? ( + "Loading..." + ) : ( + {DEVEUI.value} + )} + + )} + + + + setDevEui(value)} + style={{ marginRight: spacing }} + placeholder={DEVEUI.value} + /> + + - - - - + + Sensor messages + + + + + + + Sensor messages are{" "} + {SENSOR.value === "enable" ? ( + enabled + ) : ( + disabled + )} + . + + - - - - - Sensor is{" "} - {sensor === "enable" ? ( - enabled - ) : ( - disabled - )} - - - Lorawan status: {lorawan} - + + Lorawan status + + + + Lorawan is currently{" "} + + {LORAWAN.value?.toLowerCase()} + + . + + - - - + + + ) } const styles = StyleSheet.create({ - scrollContainer: { flex: 1, margin: 10 }, + scrollContainer: { flex: 1 }, scroll: { flex: 1 }, view: { height: 200 }, fab: { @@ -305,10 +466,8 @@ const styles = StyleSheet.create({ right: 20, }, logs: { - fontSize: 8, margin: 10, marginBottom: 20, - paddingStart: 10, }, input: { flexDirection: "row", @@ -320,12 +479,20 @@ const styles = StyleSheet.create({ flexDirection: "row", flexWrap: "wrap", alignItems: "center", - padding: 5, }, button: { margin: 5, }, bold: { - fontWeight: "900", + fontWeight: "400", + }, + heartbeat: { + flex: 1, + flexDirection: "row", + alignItems: "center", + flexWrap: "wrap", + }, + idversion: { + width: "100%", }, }) diff --git a/src/redux/slices/devicesSlice.ts b/src/redux/slices/devicesSlice.ts index 5dbf287..3229fb2 100644 --- a/src/redux/slices/devicesSlice.ts +++ b/src/redux/slices/devicesSlice.ts @@ -11,6 +11,11 @@ export interface ExtendedPeripheral extends Peripheral { signalLost?: boolean device: DeviceMetadata loading: boolean + services?: { + serviceCharacteristic: string + readCharacteristic: string + writeCharacteristic: string + } intervals: { [x: string]: NodeJS.Timeout | undefined | null } diff --git a/src/theme.ts b/src/theme.ts index 694b2e8..2bee743 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -25,7 +25,7 @@ const extendThemes = (theme: MD3Theme) => { secondary: "#fed54e", tertiary: "#ffffff", }, - padding: 20, + appPadding: 20, roundness: 10, spacing: 10, } diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 7d4fb4e..9f80d66 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,10 +1,15 @@ import dayjs from "dayjs" import { ExtendedPeripheral } from "../redux/slices/devicesSlice" import { log } from "./logger" -import { BLE_CHARACTERISTIC_WRITE_UUID, BLE_SERVICE_UUID } from "./constants" +import { + BLE_CHARACTERISTIC_READ_UUID, + BLE_CHARACTERISTIC_WRITE_UUID, + BLE_SERVICE_UUID, +} from "./constants" import BleManager from "react-native-ble-manager" import { Buffer } from "buffer" import { readlineParserEmitter } from "../hooks/useBleListeners" +import { Services } from "../ble/types" export const clearAllDeviceIntervals = ( device: ExtendedPeripheral | undefined | null, @@ -73,8 +78,9 @@ export const writeToDevice: WriteFunction = async (peripheral, data) => { await BleManager.writeWithoutResponse( peripheral.id, - BLE_SERVICE_UUID, - BLE_CHARACTERISTIC_WRITE_UUID, + peripheral.services?.serviceCharacteristic || BLE_SERVICE_UUID, + peripheral.services?.writeCharacteristic || + BLE_CHARACTERISTIC_WRITE_UUID, byteArray, ) @@ -102,3 +108,61 @@ export const writeToDevice: WriteFunction = async (peripheral, data) => { } } } + +const UUID_LENGTH = 36 + +export const extractServiceAndCharacteristic = (services?: Services) => { + log("Extracting services and characteristics.") + if (!services) { + log("Service object not found, using default.") + return { + writeCharacteristic: BLE_CHARACTERISTIC_WRITE_UUID, + readCharacteristic: BLE_CHARACTERISTIC_READ_UUID, + serviceCharacteristic: BLE_SERVICE_UUID, + } + } + + try { + const allServices = services.services.filter( + (s) => s.uuid.length === UUID_LENGTH, + ) + + if (allServices.length !== 1) { + throw new Error("Error: More then one service found.") + } + + const service = allServices[0] + + const write = services.characteristics.find((c) => { + if (c.service === service.uuid && c.properties.WriteWithoutResponse) { + return true + } + }) + + const read = services.characteristics.find((c) => { + if (c.service === service.uuid && c.properties.Notify) { + return true + } + }) + + if (!write || !read) { + throw new Error( + `Error: No combination found for this service: ${service.uuid}`, + ) + } + + return { + serviceCharacteristic: service.uuid, + readCharacteristic: read.characteristic, + writeCharacteristic: write.characteristic, + } + } catch (e: any) { + log(e.message) + log("Extracting services and characteristics failed, using default.") + return { + writeCharacteristic: BLE_CHARACTERISTIC_WRITE_UUID, + readCharacteristic: BLE_CHARACTERISTIC_READ_UUID, + serviceCharacteristic: BLE_SERVICE_UUID, + } + } +}