256 lines
7.2 KiB
TypeScript
256 lines
7.2 KiB
TypeScript
|
|
import { useEffect, useState, type ReactNode } from "react";
|
||
|
|
import { Pressable, View } from "react-native";
|
||
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||
|
|
import * as Clipboard from "expo-clipboard";
|
||
|
|
|
||
|
|
import { Container } from "@/components/Container";
|
||
|
|
import { SafeAreaScrollView } from "@/components/safe-area-scroll-view";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Text } from "@/components/ui/text";
|
||
|
|
import { Ble } from "@aris/ble";
|
||
|
|
import { useBleStore } from "@/store/ble";
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
import { Ionicons } from "@expo/vector-icons";
|
||
|
|
|
||
|
|
const formatTime = (timestamp: number | null) => {
|
||
|
|
if (!timestamp) {
|
||
|
|
return "Never";
|
||
|
|
}
|
||
|
|
return new Date(timestamp).toLocaleTimeString();
|
||
|
|
};
|
||
|
|
|
||
|
|
export default function BleScreen() {
|
||
|
|
const initialize = useBleStore((state) => state.initialize);
|
||
|
|
const isSupported = useBleStore((state) => state.isSupported);
|
||
|
|
const insets = useSafeAreaInsets();
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
initialize();
|
||
|
|
}, [initialize]);
|
||
|
|
|
||
|
|
if (!isSupported) {
|
||
|
|
return (
|
||
|
|
<View className="flex flex-1 bg-white">
|
||
|
|
<Container className="px-8">
|
||
|
|
<View className="gap-1">
|
||
|
|
<Text variant="h3">BLE</Text>
|
||
|
|
<Text className="text-sm text-muted-foreground">
|
||
|
|
Bluetooth peripheral status and controls.
|
||
|
|
</Text>
|
||
|
|
</View>
|
||
|
|
<View className="mt-4 rounded-xl border border-muted bg-muted/20 px-4 py-3">
|
||
|
|
<Text className="text-sm font-medium">BLE Unavailable</Text>
|
||
|
|
<Text className="text-sm text-muted-foreground">
|
||
|
|
This page is currently supported on iOS only.
|
||
|
|
</Text>
|
||
|
|
</View>
|
||
|
|
</Container>
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<View className="flex flex-1 bg-white">
|
||
|
|
<View>
|
||
|
|
<SafeAreaScrollView>
|
||
|
|
<View className="gap-4" style={{ paddingBottom: insets.bottom + 24 }}>
|
||
|
|
<View className="gap-1 px-8">
|
||
|
|
<Text variant="h3">BLE</Text>
|
||
|
|
<Text className="text-sm text-muted-foreground">
|
||
|
|
Bluetooth peripheral status and controls.
|
||
|
|
</Text>
|
||
|
|
</View>
|
||
|
|
|
||
|
|
<View className="w-full h-px bg-muted" />
|
||
|
|
<ControlPanel />
|
||
|
|
|
||
|
|
<View className="w-full h-px bg-muted" />
|
||
|
|
<BleStatusSection />
|
||
|
|
|
||
|
|
<View className="w-full h-px bg-muted" />
|
||
|
|
<TelemetrySection />
|
||
|
|
|
||
|
|
<View className="w-full h-px bg-muted" />
|
||
|
|
<UuidsSection />
|
||
|
|
</View>
|
||
|
|
</SafeAreaScrollView>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function ValueRow({ label, value }: { label: ReactNode; value: ReactNode }) {
|
||
|
|
return (
|
||
|
|
<View className="flex flex-row items-center justify-between">
|
||
|
|
{typeof label === "string" ? (
|
||
|
|
<Text className="text-sm text-muted-foreground">{label}</Text>
|
||
|
|
) : (
|
||
|
|
label
|
||
|
|
)}
|
||
|
|
{typeof value === "string" || typeof value === "number" ? (
|
||
|
|
<Text className="text-sm">{value}</Text>
|
||
|
|
) : (
|
||
|
|
value
|
||
|
|
)}
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function BleStatusSection() {
|
||
|
|
const bluetoothState = useBleStore((state) => state.bluetoothState);
|
||
|
|
const isSubscribed = useBleStore((state) => state.isSubscribed);
|
||
|
|
const subscribedCount = useBleStore((state) => state.subscribedCount);
|
||
|
|
const connectionLabel = isSubscribed ? "Connected" : "Not Connected";
|
||
|
|
|
||
|
|
return (
|
||
|
|
<View className="px-8">
|
||
|
|
<Text variant="h4" className="mb-2 font-medium">
|
||
|
|
Status
|
||
|
|
</Text>
|
||
|
|
<View className="gap-1">
|
||
|
|
<ValueRow label="State" value={connectionLabel} />
|
||
|
|
<ValueRow label="Bluetooth" value={bluetoothState} />
|
||
|
|
<ValueRow label="Subscribers" value={subscribedCount} />
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function ControlPanel() {
|
||
|
|
const advertisingEnabled = useBleStore((state) => state.advertisingEnabled);
|
||
|
|
const setAdvertisingEnabled = useBleStore(
|
||
|
|
(state) => state.setAdvertisingEnabled,
|
||
|
|
);
|
||
|
|
const wifiRequested = useBleStore((state) => state.wifiRequested);
|
||
|
|
const setWifiRequested = useBleStore((state) => state.setWifiRequested);
|
||
|
|
const sendFixtureFeedNow = useBleStore((state) => state.sendFixtureFeedNow);
|
||
|
|
const handleCopyUUIDs = async () => {
|
||
|
|
const uuids = Ble.getUuids();
|
||
|
|
const text = [
|
||
|
|
`SERVICE_UUID=${uuids.serviceUUID}`,
|
||
|
|
`FEED_TX_UUID=${uuids.feedTxUUID}`,
|
||
|
|
`CONTROL_RX_UUID=${uuids.controlRxUUID}`,
|
||
|
|
`WIFI_REQUEST_TX_UUID=${uuids.wifiRequestTxUUID}`,
|
||
|
|
].join("\n");
|
||
|
|
await Clipboard.setStringAsync(text);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<View className="px-8">
|
||
|
|
<Text variant="h4" className="mb-3 font-medium">
|
||
|
|
Control Panel
|
||
|
|
</Text>
|
||
|
|
|
||
|
|
<View className="flex-row gap-3 w-full mb-3">
|
||
|
|
<ControlTile
|
||
|
|
icon={<Ionicons name="radio-outline" size={20} />}
|
||
|
|
label="Advertising"
|
||
|
|
checked={advertisingEnabled}
|
||
|
|
onCheckChange={setAdvertisingEnabled}
|
||
|
|
/>
|
||
|
|
<ControlTile
|
||
|
|
icon={<Ionicons name="wifi-outline" size={20} />}
|
||
|
|
label="WiFi"
|
||
|
|
checked={wifiRequested}
|
||
|
|
onCheckChange={setWifiRequested}
|
||
|
|
/>
|
||
|
|
</View>
|
||
|
|
|
||
|
|
<View className="gap-2">
|
||
|
|
<Button onPress={sendFixtureFeedNow} variant="outline">
|
||
|
|
<Text>Send Fixture Feed Now</Text>
|
||
|
|
</Button>
|
||
|
|
<Button onPress={handleCopyUUIDs} variant="outline">
|
||
|
|
<Text>Copy UUIDs</Text>
|
||
|
|
</Button>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function ControlTile({
|
||
|
|
icon,
|
||
|
|
label,
|
||
|
|
checked,
|
||
|
|
onCheckChange,
|
||
|
|
}: {
|
||
|
|
icon: ReactNode;
|
||
|
|
label: ReactNode;
|
||
|
|
checked: boolean;
|
||
|
|
onCheckChange: (checked: boolean) => void;
|
||
|
|
}) {
|
||
|
|
const [isPressed, setIsPressed] = useState(false);
|
||
|
|
return (
|
||
|
|
<Pressable
|
||
|
|
onPressIn={() => {
|
||
|
|
setIsPressed(true);
|
||
|
|
}}
|
||
|
|
onPressOut={() => {
|
||
|
|
setIsPressed(false);
|
||
|
|
}}
|
||
|
|
onPress={() => {
|
||
|
|
onCheckChange(!checked);
|
||
|
|
}}
|
||
|
|
className={cn(
|
||
|
|
"flex-1 items-start justify-center rounded-md px-3 py-2 gap-1",
|
||
|
|
checked ? "border-2 border-primary" : "border border-border",
|
||
|
|
{ "bg-accent": isPressed },
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{icon}
|
||
|
|
<Text className={cn({ "font-bold": checked })}>{label}</Text>
|
||
|
|
</Pressable>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function TelemetrySection() {
|
||
|
|
const lastMsgIdSent = useBleStore((state) => state.lastMsgIdSent);
|
||
|
|
const lastPingAt = useBleStore((state) => state.lastPingAt);
|
||
|
|
const lastDataAt = useBleStore((state) => state.lastDataAt);
|
||
|
|
const lastNotifyAt = useBleStore((state) => state.lastNotifyAt);
|
||
|
|
const notifyQueueDepth = useBleStore((state) => state.notifyQueueDepth);
|
||
|
|
const droppedNotifyPackets = useBleStore(
|
||
|
|
(state) => state.droppedNotifyPackets,
|
||
|
|
);
|
||
|
|
const lastCommand = useBleStore((state) => state.lastCommand);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<View className="px-8">
|
||
|
|
<Text variant="h4" className="mb-2 font-medium">
|
||
|
|
Telemetry
|
||
|
|
</Text>
|
||
|
|
<View className="gap-1">
|
||
|
|
<ValueRow label="Last msgId" value={lastMsgIdSent} />
|
||
|
|
<ValueRow label="Last ping" value={formatTime(lastPingAt)} />
|
||
|
|
<ValueRow label="Last data" value={formatTime(lastDataAt)} />
|
||
|
|
<ValueRow label="Notify queue" value={notifyQueueDepth} />
|
||
|
|
{droppedNotifyPackets > 0 ? (
|
||
|
|
<ValueRow
|
||
|
|
label="Dropped notify packets"
|
||
|
|
value={droppedNotifyPackets}
|
||
|
|
/>
|
||
|
|
) : null}
|
||
|
|
<ValueRow label="Last notify" value={formatTime(lastNotifyAt)} />
|
||
|
|
{lastCommand ? (
|
||
|
|
<ValueRow label="Last control" value={lastCommand} />
|
||
|
|
) : null}
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function UuidsSection() {
|
||
|
|
const uuids = Ble.getUuids();
|
||
|
|
|
||
|
|
return (
|
||
|
|
<View className="px-8">
|
||
|
|
<Text variant="h4" className="mb-2 font-medium">
|
||
|
|
UUIDs
|
||
|
|
</Text>
|
||
|
|
<Text selectable className="text-xs text-muted-foreground">
|
||
|
|
{`SERVICE_UUID=${uuids.serviceUUID}\nFEED_TX_UUID=${uuids.feedTxUUID}\nCONTROL_RX_UUID=${uuids.controlRxUUID}\nWIFI_REQUEST_TX_UUID=${uuids.wifiRequestTxUUID}`}
|
||
|
|
</Text>
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
}
|