powerfun-setting/src/InfoScreen.tsx
2025-12-11 15:00:38 +08:00

418 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState, useCallback, useRef } from "react";
import {
View,
Text as RNText,
TextInput,
ScrollView,
StyleSheet,
ActivityIndicator,
useColorScheme,
Alert,
Pressable,
} from "react-native";
import { useFocusEffect } from "@react-navigation/native";
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { RootStackParamList } from "../App";
import {
Central,
ScannedPeripheral,
ConnectionStatus,
} from "@systemic-games/react-native-bluetooth-le";
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
type Props = NativeStackScreenProps<RootStackParamList, "Info">;
const powerServiceUuid = "fff1";
const powerWriteUuid = "fff2";
const powerNotifyUuid = "fff3";
const fullUUID = (uuid: string) =>
uuid.length === 4
? `0000${uuid}-0000-1000-8000-00805f9b34fb`
: uuid;
const Text = ({ children, style, ...props }: any) => {
const isDark = useColorScheme() === "dark";
return (
<RNText
style={[{ color: isDark ? "white" : "black", fontSize: 16 }, style]}
{...props}
>
{children}
</RNText>
);
};
export default function InfoScreen({ route, navigation }: Props) {
const { peripheral } = route.params;
const deviceKey = peripheral.address || peripheral.systemId;
const [isConnected, setIsConnected] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [serial, setSerial] = useState("读取中...");
const [firmware, setFirmware] = useState("读取中...");
const [hardware, setHardware] = useState("读取中...");
const [battery, setBattery] = useState("读取中...");
const [powerTrim, setPowerTrim] = useState("读取中");
const [inputTrim, setInputTrim] = useState("100");
const [readSuccessToast, setReadSuccessToast] = useState(false); // ✅ 读取成功提示
const [powerTrimLoading, setPowerTrimLoading] = useState(false); // 正在写入功率微调
const [powerTrimSuccessToast, setPowerTrimSuccessToast] = useState(false); // 功率微调写入成功
const notifySubscribedRef = useRef(false);
const disconnectingRef = useRef(false);
const prevConnectedRef = useRef(isConnected);
const isActiveRef = useRef(true);
useFocusEffect(
React.useCallback(() => {
// 页面获得焦点
isActiveRef.current = true;
return () => {
// 页面失去焦点
isActiveRef.current = false;
};
}, [])
);
// ========== 监听连接状态变化,断开时提示重新连接 ==========
useEffect(() => {
if (prevConnectedRef.current && !isConnected && isActiveRef.current) {
Alert.alert("提示", "请重新连接设备", [
{ text: "确定", onPress: () => navigation.goBack() },
]);
}
prevConnectedRef.current = isConnected;
}, [isConnected]);
// ========== 页面即将返回时,强制断开蓝牙 ==========
useEffect(() => {
const unsubscribe = navigation.addListener("beforeRemove", async (e) => {
if (disconnectingRef.current) return;
disconnectingRef.current = true;
const next = e.data.action?.type;
const action = e.data.action as any;
// ✅ 判断是否跳转到 Dfu 页面
if (next === "NAVIGATE" && action?.payload?.name === "Dfu") {
console.log("➡️ 跳转到 DFU不断开蓝牙");
disconnectingRef.current = false;
return;
}
// 🔌 其他页面离开则断开蓝牙
console.log("📴 页面关闭,断开蓝牙...");
try {
await Central.disconnectPeripheral(peripheral);
console.log("✅ 已断开蓝牙");
} catch (err) {
console.warn("❌ 断开失败:", err);
} finally {
disconnectingRef.current = false;
}
});
return unsubscribe;
}, [navigation, peripheral]);
// ========== BLE notify 订阅 ==========
useEffect(() => {
const connectionHandler = async (ev: {
peripheral: ScannedPeripheral;
connectionStatus: ConnectionStatus;
}) => {
const addr = ev.peripheral.address || ev.peripheral.systemId;
if (addr !== deviceKey) return;
console.log("🔌 connection event:", ev.connectionStatus);
setIsConnected(
ev.connectionStatus === "connected" || ev.connectionStatus === "ready"
);
if (ev.connectionStatus === "ready" && !notifySubscribedRef.current) {
console.log("✅ device ready - subscribe notify FFF3");
try {
await Central.unsubscribeCharacteristic(
peripheral,
fullUUID(powerServiceUuid),
fullUUID(powerNotifyUuid)
).catch(() => {});
await Central.subscribeCharacteristic(
peripheral,
fullUUID(powerServiceUuid),
fullUUID(powerNotifyUuid),
(notifyEv) => {
try {
const raw = (notifyEv as any).value;
if (!raw) return;
const arr =
raw instanceof Uint8Array
? Array.from(raw)
: Array.from(new Uint8Array(raw));
if (arr[0] === 0x02 && arr.length >= 2) {
setPowerTrim(arr[1].toString());
setInputTrim(arr[1].toString());
}
} catch (err) {
console.warn("notify parse error", err);
}
}
);
notifySubscribedRef.current = true;
console.log("✅ notify 已订阅");
} catch (err) {
console.warn("❌ notify 订阅失败:", err);
}
}
};
Central.addListener("peripheralConnectionStatus", connectionHandler);
return () => {
Central.removeListener("peripheralConnectionStatus", connectionHandler);
notifySubscribedRef.current = false;
};
}, [deviceKey, peripheral]);
// ========== 首次连接并读取信息 ==========
useEffect(() => {
(async () => {
setIsLoading(true);
try {
await Central.connectPeripheral(peripheral);
await new Promise<void>((resolve) => setTimeout(resolve, 500));
const readStr = async (srv: string, char: string) => {
try {
const v = await Central.readCharacteristic(
peripheral,
fullUUID(srv),
fullUUID(char)
);
return v ? String.fromCharCode(...(v as any)) : "未知";
} catch {
return "未知";
}
};
setSerial(await readStr("180a", "2a25"));
setFirmware(await readStr("180a", "2a28"));
setHardware(await readStr("180a", "2a27"));
try {
const v = await Central.readCharacteristic(
peripheral,
fullUUID("180f"),
fullUUID("2a19")
);
if (v && (v as any).length) setBattery(`${(v as any)[0]}%`);
} catch {
setBattery("未知");
}
console.log("✅ info 读取完成");
try {
await Central.writeCharacteristic(
peripheral,
fullUUID(powerServiceUuid),
fullUUID(powerWriteUuid),
new Uint8Array([0x04]).buffer,
{ withoutResponse: false }
);
console.log("✅ 已发送 0x04等待 notify 更新功率微调");
// notify 回调里已经订阅了,所以这里不用再重复订阅
// 可以稍等 300ms确保 notify 回来后 UI 会更新
await new Promise<void>(resolve => setTimeout(() => resolve(), 300));
} catch (err) {
console.warn("❌ 首次读取功率微调失败", err);
}
} catch (e) {
console.warn("❌ 读取失败", e);
} finally {
setIsLoading(false);
// ✅ 显示读取成功提示 2 秒
setReadSuccessToast(true);
setTimeout(() => setReadSuccessToast(false), 2000);
}
})();
}, [peripheral]);
// ========== 写入功率微调 ==========
const updatePowerTrim = async () => {
const val = parseInt(inputTrim);
if (isNaN(val) || val < 50 || val > 200) {
Alert.alert("提示", "请输入有效数字50~200");
return;
}
if (val === Number(powerTrim)) {
console.log("⚙️ 相同数值,跳过发送");
return;
}
if (!isConnected) {
Alert.alert("提示", "设备未连接");
return;
}
try {
setPowerTrimLoading(true);
console.log("🚀 写入功率微调", val);
await Central.writeCharacteristic(
peripheral,
fullUUID(powerServiceUuid),
fullUUID(powerWriteUuid),
new Uint8Array([0x02, val]).buffer,
{ withoutResponse: false }
);
await new Promise<void>((resolve) => setTimeout(() => resolve(), 150));
await Central.writeCharacteristic(
peripheral,
fullUUID(powerServiceUuid),
fullUUID(powerWriteUuid),
new Uint8Array([0x04]).buffer,
{ withoutResponse: false }
);
setPowerTrimLoading(false);
setPowerTrimSuccessToast(true);
setTimeout(() => setPowerTrimSuccessToast(false), 2000);
} catch (err) {
console.warn("❌ 写入失败", err);
Alert.alert("写入失败");
}
};
// ========== UI ==========
return (
<ScrollView contentContainerStyle={{ padding: 20, gap: 8 }}>
<View style={styles.row}>
<Text>: {peripheral.name}</Text>
</View>
<View style={styles.row}>
<Text>ID号: {serial}</Text>
</View>
<View style={styles.row}>
<Text>: {firmware}</Text>
</View>
<View style={styles.row}>
<Text>: {battery}</Text>
</View>
<View style={styles.row}>
<Text>: {isConnected ? "已连接" : "未连接"}</Text>
</View>
<View style={{ marginTop: 20 }}>
<Text>: {powerTrim}%</Text>
<TextInput
style={styles.input}
value={inputTrim}
onChangeText={setInputTrim}
keyboardType="numeric"
onEndEditing={updatePowerTrim}
/>
<Pressable onPress={updatePowerTrim} style={styles.pressable} disabled={isLoading || powerTrimLoading}>
<Text style={{ color: "white" }}></Text>
</Pressable>
</View>
<View style={{ marginTop: 30 }}>
<Pressable
onPress={() => {
if (!isConnected) {
Alert.alert("提示", "请重新连接设备", [
{
text: "确定",
onPress: () => {
// 返回 ScanScreen
navigation.goBack();
},
},
]);
return;
}
// 蓝牙已连接,正常跳转 DFU
navigation.navigate("Dfu", {
deviceId: deviceKey,
name: peripheral.name,
firmware,
});
}}
style={styles.pressable}
disabled={isLoading || powerTrimLoading} // ❌ 禁用点击
>
<Text style={{ color: "white" }}></Text>
</Pressable>
</View>
{isLoading && (
<View style={{ marginVertical: 60, alignItems: "center" }}>
<ActivityIndicator size={80} color="#E7141E" />
<Text style={{ marginTop: 10, fontSize: 16 }}>...</Text>
</View>
)}
{/* ✅ 读取成功提示,固定在页面底部 */}
{readSuccessToast && (
<View style={{ marginVertical: 60, alignItems: "center" }}>
<Icon name="check-circle" size={60} color="#E7141E" />
<Text style={{ marginTop: 10, fontSize: 16 }}></Text>
</View>
)}
{/* 正在写入功率微调 */}
{powerTrimLoading && (
<View style={{ marginVertical: 60, alignItems: "center" }}>
<ActivityIndicator size={60} color="#E7141E" />
<Text style={{ marginTop: 10, fontSize: 16 }}>...</Text>
</View>
)}
{/* 功率微调写入成功 */}
{powerTrimSuccessToast && (
<View style={{ marginVertical: 60, alignItems: "center" }}>
<Icon name="check-circle" size={60} color="#E7141E" />
<Text style={{ marginTop: 10, fontSize: 16 }}></Text>
</View>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
row: {
borderBottomWidth: 1,
borderBottomColor: "#E7141E",
paddingBottom: 4, // 横线和文字保持一定间距
marginBottom: 8, // 行间距
},
input: {
borderWidth: 1,
borderColor: "#ccc",
padding: 6,
marginTop: 5,
borderRadius: 4,
},
pressable: {
padding: 10,
backgroundColor: "#E7141E",
marginTop: 10,
borderRadius: 6,
alignItems: "center",
},
});