// InfoScreen2.tsx 此页面为桨频器信息页面 import React, { useEffect, useRef, useState } from "react"; import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View, Alert, } from "react-native"; import { NativeStackScreenProps } from "@react-navigation/native-stack"; import { decode as atob } from "base-64"; import { Central, ScannedPeripheral, ConnectionStatus, } from "@systemic-games/react-native-bluetooth-le"; import { RootStackParamList } from "../App"; import { useTranslation } from "react-i18next"; import MyStatusbar from "./component/MyStatusbar"; import MyHeader from "./component/MyHeader"; type Props = NativeStackScreenProps; const CSC_SERVICE_UUID = "1816"; const CSC_MEASUREMENT_UUID = "2a5b"; const CUSTOM_SERVICE_UUID = "fff1"; const CUSTOM_WRITE_UUID = "fff2"; const CUSTOM_NOTIFY_UUID = "fff3"; const DFU_MANIFEST_URL = "https://powerfun.oss-cn-shanghai.aliyuncs.com/yecongdfu/latest.json"; type BoatType = 0x00 | 0x01 | 0x02; type ParsedFirmware = { hardware: number; iteration: number; build: string; raw: string; }; type DeviceInfo = { hardware: number; latestFirmware: string; download: string; }; const fullUUID = (uuid: string) => uuid.length === 4 ? `0000${uuid}-0000-1000-8000-00805f9b34fb` : uuid; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const base64ToBytes = (base64: string): number[] => { const binary = atob(base64); const bytes: number[] = []; for (let i = 0; i < binary.length; i += 1) { bytes.push(binary.charCodeAt(i) & 0xff); } return bytes; }; const bytesToArrayBuffer = (bytes: number[]): ArrayBuffer => { return new Uint8Array(bytes).buffer; }; const normalizeBytes = (arr: number[]): number[] => { return arr.map((v) => v & 0xff); }; const toHexByte = (value: number): string => (value & 0xff).toString(16).toUpperCase().padStart(2, "0"); const bytesToHexString = (bytes: number[]): string => normalizeBytes(bytes) .map((v) => toHexByte(v)) .join(" "); const parseFirmwareVersion = (text: string): ParsedFirmware => { const raw = String(text ?? "").trim(); const parts = raw.split("."); if (parts.length < 2) { throw new Error(`固件版本格式不正确: ${raw}`); } const hardware = parseInt(parts[0], 10); const iteration = parseInt(parts[1], 10); const build = parts.length >= 3 ? parts[2] : ""; if (Number.isNaN(hardware) || Number.isNaN(iteration)) { throw new Error(`固件版本无法解析: ${raw}`); } return { hardware, iteration, build, raw, }; }; export default function InfoScreen2({ route, navigation }: Props) { const { t } = useTranslation(); const { peripheral } = route.params; const getBoatNameByValue = (value?: number | null): string => { if (value === 0x00) return t("info2.boatKayak"); if (value === 0x01) return t("info2.boatRowing"); if (value === 0x02) return t("info2.boatRacing"); return t("common.unknown"); }; const deviceKey = typeof peripheral.address === "string" && peripheral.address.length > 0 ? peripheral.address : String(peripheral.systemId ?? ""); const [cadence, setCadence] = useState(0); const [isLoading, setIsLoading] = useState(true); const [selectedBoat, setSelectedBoat] = useState(0x00); const [boatBusy, setBoatBusy] = useState(false); const [boatStatus, setBoatStatus] = useState(t("info2.waitingAction")); const [boatNotifyHex, setBoatNotifyHex] = useState("--"); const [boatVerified, setBoatVerified] = useState("--"); const [isConnected, setIsConnected] = useState(false); const [serial, setSerial] = useState(t("info.reading")); const [firmware, setFirmware] = useState(t("info.reading")); const [battery, setBattery] = useState(t("info.reading")); // 新增:固件升级状态 const [latestFirmware, setLatestFirmware] = useState(t("info.reading")); const [upgradeAvailable, setUpgradeAvailable] = useState(false); const [upgradeCheckDone, setUpgradeCheckDone] = useState(false); const lastCrankRef = useRef(null); const lastEventTimeRef = useRef(null); const lastCadenceUpdateAppTsRef = useRef(null); const boatReadResolverRef = useRef<((bytes: number[]) => void) | null>(null); const mountedRef = useRef(true); const disconnectingRef = useRef(false); const hasConnectedOnceRef = useRef(false); const hasNavigatedBackRef = useRef(false); const skipDisconnectOnLeaveRef = useRef(false); const checkUpgradeNeeded = async (firmwareText: string) => { try { const text = String(firmwareText ?? "").trim(); if (!text) { if (mountedRef.current) { setLatestFirmware(t("common.unknown")); setUpgradeAvailable(false); setUpgradeCheckDone(true); } return; } const currentFw = parseFirmwareVersion(text); console.log("🔥 InfoScreen currentFw =", currentFw); const manifestResp = await fetch(DFU_MANIFEST_URL); console.log("🔥 InfoScreen manifest status =", manifestResp.status); if (!manifestResp.ok) { throw new Error(`manifest 下载失败,HTTP ${manifestResp.status}`); } const manifestText = await manifestResp.text(); console.log("🔥 InfoScreen manifest text =", manifestText); let manifest: { devices: DeviceInfo[] }; try { manifest = JSON.parse(manifestText) as { devices: DeviceInfo[] }; } catch (e) { throw new Error("manifest 不是合法 JSON"); } const deviceInfo = manifest.devices.find( (d) => d.hardware === currentFw.hardware ); console.log("🔥 InfoScreen matched deviceInfo =", deviceInfo); if (!deviceInfo) { if (mountedRef.current) { setLatestFirmware(t("info2.notMatched")); setUpgradeAvailable(false); setUpgradeCheckDone(true); } return; } const latestFw = parseFirmwareVersion(deviceInfo.latestFirmware); console.log("🔥 InfoScreen latestFw =", latestFw); if (latestFw.hardware !== currentFw.hardware) { throw new Error( `服务器固件硬件号不匹配:当前 ${currentFw.hardware},服务器 ${latestFw.hardware}` ); } const needUpgrade = latestFw.iteration > currentFw.iteration; if (mountedRef.current) { setLatestFirmware(deviceInfo.latestFirmware); setUpgradeAvailable(needUpgrade); setUpgradeCheckDone(true); } } catch (err: any) { console.warn("⚠️ checkUpgradeNeeded failed:", err); if (mountedRef.current) { setLatestFirmware(t("info2.checkFailed")); setUpgradeAvailable(false); setUpgradeCheckDone(true); } } }; const readDeviceInfo = async () => { console.log("📘 start read device info..."); try { try { const v = await Central.readCharacteristic( peripheral, fullUUID("180a"), fullUUID("2a25") ); if (v && (v as any).length) { const idText = String.fromCharCode(...(v as any)); console.log("📘 serial raw:", v, "=>", idText); if (mountedRef.current) { setSerial(idText); } } else if (mountedRef.current) { setSerial( (peripheral.address || peripheral.systemId || peripheral.name || t("common.unknown")) as string ); } } catch (err) { console.warn("⚠️ read serial failed:", err); if (mountedRef.current) { setSerial( (peripheral.address || peripheral.systemId || peripheral.name || t("common.unknown")) as string ); } } try { const v = await Central.readCharacteristic( peripheral, fullUUID("180a"), fullUUID("2a28") ); if (v && (v as any).length) { const fwText = String.fromCharCode(...(v as any)); console.log("📘 firmware raw:", v, "=>", fwText); if (mountedRef.current) { setFirmware(fwText); } // 新增:在 Info 页提前检查是否需要升级 await checkUpgradeNeeded(fwText); } else if (mountedRef.current) { setFirmware(t("common.unknown")); setLatestFirmware(t("common.unknown")); setUpgradeAvailable(false); setUpgradeCheckDone(true); } } catch (err) { console.warn("⚠️ read firmware failed:", err); if (mountedRef.current) { setFirmware(t("common.unknown")); setLatestFirmware(t("common.unknown")); setUpgradeAvailable(false); setUpgradeCheckDone(true); } } try { const v = await Central.readCharacteristic( peripheral, fullUUID("180f"), fullUUID("2a19") ); if (v && (v as any).length) { const batteryValue = (v as any)[0]; console.log("📘 battery raw:", v, "=>", batteryValue); if (mountedRef.current) { setBattery(`${batteryValue}%`); } } else if (mountedRef.current) { setBattery(t("common.unknown")); } } catch (err) { console.warn("⚠️ read battery failed:", err); if (mountedRef.current) { setBattery(t("common.unknown")); } } } catch (err) { console.warn("⚠️ readDeviceInfo failed:", err); if (mountedRef.current) { setSerial( (peripheral.address || peripheral.systemId || peripheral.name || t("common.unknown")) as string ); setFirmware(t("common.unknown")); setBattery(t("common.unknown")); setLatestFirmware(t("common.unknown")); setUpgradeAvailable(false); setUpgradeCheckDone(true); } } }; const writeBytesToFff2 = async (bytes: number[]) => { const normalized = normalizeBytes(bytes); const buffer = bytesToArrayBuffer(normalized); console.log("✍️ write FFF2 hex:", bytesToHexString(normalized)); await Central.writeCharacteristic( peripheral, fullUUID(CUSTOM_SERVICE_UUID), fullUUID(CUSTOM_WRITE_UUID), buffer, { withoutResponse: false } ); }; const waitBoatNotifyOnce = (timeoutMs = 3000): Promise => { return new Promise((resolve, reject) => { const timer = setTimeout(() => { if (boatReadResolverRef.current) { boatReadResolverRef.current = null; } reject(new Error(t("info2.waitFff3Timeout"))); }, timeoutMs); boatReadResolverRef.current = (bytes: number[]) => { const normalized = normalizeBytes(bytes); if (normalized.length < 1) { console.log("⚠️ 忽略 FFF3:长度不足 1 字节"); return; } clearTimeout(timer); boatReadResolverRef.current = null; resolve(normalized); }; }); }; const applyBoatTypeFromNotify = (notifyBytes: number[]) => { let returnedBoatType: number | null = null; if (notifyBytes.length === 1) { returnedBoatType = notifyBytes[0]; } else if (notifyBytes[0] === 0x01 && notifyBytes.length >= 2) { returnedBoatType = notifyBytes[1]; } else { returnedBoatType = notifyBytes[0]; } if ( returnedBoatType === 0x00 || returnedBoatType === 0x01 || returnedBoatType === 0x02 ) { setSelectedBoat(returnedBoatType as BoatType); setBoatVerified( t("info2.currentBoat", { boatName: getBoatNameByValue(returnedBoatType) }) ); setBoatStatus( t("info2.readCurrentBoat", { boatName: getBoatNameByValue(returnedBoatType), }) ); console.log( "🚣 apply boat type:", returnedBoatType, getBoatNameByValue(returnedBoatType) ); } else { setBoatVerified(`❌ ${t("info2.unknownBoatType")}`); setBoatStatus(t("info2.readAbnormal", { hex: bytesToHexString(notifyBytes) })); console.log("⚠️ unknown boat notify:", bytesToHexString(notifyBytes)); } }; const handleSetBoatType = async (boatType: BoatType) => { if (boatBusy) return; try { setBoatBusy(true); setBoatVerified("--"); setBoatNotifyHex("--"); const boatName = getBoatNameByValue(boatType); setBoatStatus(t("info2.writingBoat", { boatName })); await writeBytesToFff2([0x01, boatType]); setBoatStatus(t("info2.writtenWaitRead", { boatName })); await sleep(200); setBoatStatus(t("info2.requestingBoatRead")); const waitPromise = waitBoatNotifyOnce(3000); await writeBytesToFff2([0x02]); const notifyBytes = await waitPromise; const hex = bytesToHexString(notifyBytes); setBoatNotifyHex(hex); let returnedBoatType: number | null = null; if (notifyBytes.length === 1) { returnedBoatType = notifyBytes[0]; } else if (notifyBytes[0] === 0x01) { returnedBoatType = notifyBytes[1]; } else { returnedBoatType = notifyBytes[0]; } const ok = returnedBoatType === boatType; if (ok) { applyBoatTypeFromNotify(notifyBytes); setBoatStatus(t("info2.setSuccess", { boatName })); } else { setBoatVerified( `❌ ${t("info2.notMatch", { expected: boatName, actual: getBoatNameByValue(returnedBoatType), })}` ); setBoatStatus(t("info2.readResult", { hex })); } } catch (error: any) { console.warn("❌ set boat type failed:", error); setBoatStatus( t("info2.actionFailed", { message: error?.message ?? "unknown error" }) ); setBoatVerified(`❌ ${t("info2.failed")}`); } finally { setBoatBusy(false); } }; const goToDfu = () => { if (!isConnected) { Alert.alert(t("common.notice"), t("info2.disconnectedNeedReconnect"), [ { text: t("info.confirm"), onPress: () => { navigation.goBack(); }, }, ]); return; } skipDisconnectOnLeaveRef.current = true; console.log("🔥 peripheral =", JSON.stringify(peripheral, null, 2)); console.log("🔥 peripheral.address =", peripheral.address); console.log("🔥 peripheral.systemId =", peripheral.systemId); console.log("🔥 peripheral.id =", peripheral.id); console.log("🔥 peripheral.identifier =", peripheral.identifier); console.log("🔥 peripheral.uuid =", peripheral.uuid); console.log("🔥 current deviceKey =", deviceKey); navigation.navigate("Dfu", { deviceId: deviceKey, systemId: peripheral.systemId, address: peripheral.address, name: peripheral.name ?? "", firmware: firmware ?? "", }); }; 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); const connected = ev.connectionStatus === "connected" || ev.connectionStatus === "ready"; setIsConnected(connected); if (connected) { hasConnectedOnceRef.current = true; return; } if ( hasConnectedOnceRef.current && mountedRef.current && !disconnectingRef.current && !hasNavigatedBackRef.current ) { hasNavigatedBackRef.current = true; console.log("↩️ 检测到设备断开,自动返回上一级"); if (navigation.canGoBack()) { navigation.goBack(); } } }; Central.addListener("peripheralConnectionStatus", connectionHandler); return () => { Central.removeListener("peripheralConnectionStatus", connectionHandler); }; }, [deviceKey, navigation]); 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; 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]); useEffect(() => { mountedRef.current = true; let cscSubscription: any = null; let customSubscription: any = null; let cadenceZeroTimer: ReturnType | null = null; const handleMeasurement = (ev: any) => { const rawData: number[] = Array.isArray(ev?.value) ? ev.value : typeof ev?.value === "string" ? base64ToBytes(ev.value) : ev?.value instanceof ArrayBuffer ? Array.from(new Uint8Array(ev.value)) : []; const data = normalizeBytes(rawData); if (data.length === 0) { console.log("📥 CSC raw data: "); return; } console.log("📥 CSC raw data hex:", bytesToHexString(data)); if (data.length < 5) { console.log("⚠️ invalid CSC packet length:", data.length); return; } const flags = data[0]; if (flags !== 0x02) { console.log("⚠️ unexpected CSC flags:", toHexByte(flags)); return; } const crankRevolutions = data[1] | (data[2] << 8); const crankEventTime = data[3] | (data[4] << 8); const lastCrank = lastCrankRef.current; const lastEventTime = lastEventTimeRef.current; console.log( "📊 CSC parsed:", "crankRevolutions=", crankRevolutions, "crankEventTime=", crankEventTime, "lastCrank=", lastCrank, "lastEventTime=", lastEventTime ); if (lastCrank !== null && lastEventTime !== null) { let deltaCrank = crankRevolutions - lastCrank; let deltaTime = crankEventTime - lastEventTime; if (deltaCrank < 0) deltaCrank += 0x10000; if (deltaTime < 0) deltaTime += 0x10000; console.log( "📊 CSC delta:", "deltaCrank=", deltaCrank, "deltaTime=", deltaTime ); if (deltaCrank > 0 && deltaTime > 0) { if (deltaCrank <= 4) { const rpm = (60 * 1024 * deltaCrank) / deltaTime; console.log("📊 CSC rpm raw:", rpm); if (rpm >= 1 && rpm <= 220) { const roundedCadence = Math.round(rpm); const appNow = Date.now(); if (mountedRef.current) { setCadence(roundedCadence); } lastCadenceUpdateAppTsRef.current = appNow; console.log( "✅ cadence updated:", "cadence=", roundedCadence, "appTs=", appNow, "bleCrank=", crankRevolutions, "bleEventTime=", crankEventTime ); } else { console.log("⚠️ rpm out of range, ignored:", rpm); } } else { console.log("⚠️ deltaCrank too large, ignored:", deltaCrank); } } else { console.log( "⚠️ delta invalid, ignored:", "deltaCrank=", deltaCrank, "deltaTime=", deltaTime ); } } else { console.log( "🟡 first valid CSC packet, baseline saved only:", "crankRevolutions=", crankRevolutions, "crankEventTime=", crankEventTime ); } lastCrankRef.current = crankRevolutions; lastEventTimeRef.current = crankEventTime; }; const handleBoatNotify = (ev: any) => { const rawData: number[] = Array.isArray(ev?.value) ? ev.value : typeof ev?.value === "string" ? base64ToBytes(ev.value) : ev?.value instanceof ArrayBuffer ? Array.from(new Uint8Array(ev.value)) : []; const data = normalizeBytes(rawData); if (data.length === 0) { console.log("📥 FFF3 raw data: "); return; } const hex = bytesToHexString(data); console.log("📥 FFF3 raw data hex:", hex); if (mountedRef.current) { setBoatNotifyHex(hex); } if (boatReadResolverRef.current) { boatReadResolverRef.current(data); } }; (async () => { setIsLoading(true); try { await Central.connectPeripheral(peripheral); await new Promise((resolve) => setTimeout(resolve, 500)); setIsConnected(true); hasConnectedOnceRef.current = true; hasNavigatedBackRef.current = false; console.log("📡 subscribe CSC:", CSC_MEASUREMENT_UUID); cscSubscription = await Central.subscribeCharacteristic( peripheral, fullUUID(CSC_SERVICE_UUID), fullUUID(CSC_MEASUREMENT_UUID), handleMeasurement ); console.log("📡 subscribe FFF3:", CUSTOM_NOTIFY_UUID); customSubscription = await Central.subscribeCharacteristic( peripheral, fullUUID(CUSTOM_SERVICE_UUID), fullUUID(CUSTOM_NOTIFY_UUID), handleBoatNotify ); await readDeviceInfo(); cadenceZeroTimer = setInterval(() => { const lastUpdateTs = lastCadenceUpdateAppTsRef.current; const now = Date.now(); if (lastUpdateTs === null) { return; } const elapsed = now - lastUpdateTs; if (elapsed > 3000) { if (mountedRef.current) { setCadence((prev) => { if (prev !== 0) { console.log( "⏱️ cadence reset to 0 and baseline cleared:", "now=", now, "lastCadenceUpdateAppTs=", lastUpdateTs, "elapsedMs=", elapsed ); return 0; } return prev; }); } lastCrankRef.current = null; lastEventTimeRef.current = null; lastCadenceUpdateAppTsRef.current = null; } }, 500); console.log("✅ all subscribe success"); try { console.log("🔎 default read boat type: write FFF2 = 02"); const waitPromise = waitBoatNotifyOnce(3000); await writeBytesToFff2([0x02]); const notifyBytes = await waitPromise; if (mountedRef.current) { setBoatNotifyHex(bytesToHexString(notifyBytes)); applyBoatTypeFromNotify(notifyBytes); } } catch (err) { console.warn("⚠️ default read boat type failed:", err); if (mountedRef.current) { setBoatStatus(t("info2.defaultReadFailed")); } } if (mountedRef.current) { setIsLoading(false); } } catch (e) { console.warn("❌ 读取失败", e); if (mountedRef.current) { setIsLoading(false); setIsConnected(false); setBoatStatus(t("info2.connectOrReadFailed")); Alert.alert(t("common.notice"), t("info2.connectOrReadFailedAlert")); } } })(); return () => { mountedRef.current = false; hasConnectedOnceRef.current = false; hasNavigatedBackRef.current = false; setIsConnected(false); try { if (cscSubscription?.remove) { cscSubscription.remove(); } } catch (err) { console.warn("⚠️ csc subscription remove failed:", err); } try { if (customSubscription?.remove) { customSubscription.remove(); } } catch (err) { console.warn("⚠️ custom subscription remove failed:", err); } if (cadenceZeroTimer) { clearInterval(cadenceZeroTimer); cadenceZeroTimer = null; } boatReadResolverRef.current = null; lastCrankRef.current = null; lastEventTimeRef.current = null; lastCadenceUpdateAppTsRef.current = null; try { if (!skipDisconnectOnLeaveRef.current) { Central.disconnectPeripheral(peripheral); } else { console.log("➡️ cleanup: 跳转到 DFU,不断开蓝牙"); } } catch {} }; }, [peripheral]); return ( <> {isLoading ? ( ) : ( <> {/* 1. 桨频数据框 */} {t("info2.cadence")} {cadence} {t("info2.cadenceUnit")} {/* 2. 船型选择 */} {t("info2.boatSelect")} {boatBusy ? ( {t("info2.boatSwitching")} ) : boatVerified.startsWith("✅") ? ( ✅ ) : boatVerified.startsWith("❌") ? ( ❌{t("info2.retry")} ) : ( - )} handleSetBoatType(0x00)} disabled={boatBusy} > {t("info2.boatKayak")} handleSetBoatType(0x01)} disabled={boatBusy} > {t("info2.boatRowing")} handleSetBoatType(0x02)} disabled={boatBusy} > {t("info2.boatRacing")} {/* 3. 设备信息(内含固件升级按钮) */} {t("info.title")} {t("info.bluetoothName")} {peripheral?.name || t("common.unknown")} {t("info.idNumber")} {serial} {t("info.firmwareVersion")} {firmware} {t("info.firmwareUpgrade")} {t("info.battery")} {battery} {t("info2.latestFirmware")} {upgradeCheckDone ? latestFirmware : t("info2.checking")} {t("info.connectionStatus")} {isConnected ? t("info.connected") : t("info.disconnected")} )} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#f2f3f7", }, contentContainer: { padding: 16, gap: 12, backgroundColor: "#f2f3f7", }, cadenceCard: { backgroundColor: "#FFFFFF", borderRadius: 12, borderWidth: 1, borderColor: "#E7141E", paddingVertical: 58, paddingHorizontal: 20, alignItems: "center", marginBottom: 4, }, cadenceLabel: { color: "#666666", fontSize: 20, marginBottom: 12, fontWeight: "500", }, cadenceValue: { color: "#E7141E", fontSize: 100, fontWeight: "700", lineHeight: 108, }, cadenceUnit: { color: "#666666", fontSize: 18, marginTop: 10, }, sectionCard: { backgroundColor: "#FFFFFF", borderRadius: 12, borderWidth: 1, borderColor: "#E7141E", padding: 16, marginBottom: 4, }, sectionHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 16, }, sectionTitle: { color: "#111111", fontSize: 16, fontWeight: "600", }, statusContainer: { flexDirection: "row", alignItems: "center", }, statusTextBusy: { color: "#FF9800", fontSize: 14, fontWeight: "500", }, statusTextSuccess: { color: "#4CAF50", fontSize: 20, fontWeight: "bold", }, statusTextError: { color: "#F44336", fontSize: 16, fontWeight: "bold", }, statusText: { color: "#444444", fontSize: 14, }, buttonRow: { flexDirection: "row", justifyContent: "space-between", gap: 10, }, boatButton: { flex: 1, backgroundColor: "#FFFFFF", borderWidth: 1, borderColor: "#E7141E", borderRadius: 10, paddingVertical: 10, alignItems: "center", }, boatButtonActive: { backgroundColor: "#E7141E", }, boatButtonText: { color: "#E7141E", fontSize: 15, fontWeight: "600", }, boatButtonTextActive: { color: "#FFFFFF", }, infoCard: { backgroundColor: "#FFFFFF", borderRadius: 12, borderWidth: 1, borderColor: "#E7141E", paddingHorizontal: 14, paddingVertical: 4, }, infoHeader: { paddingBottom: 4, }, infoTitle: { color: "#111111", fontSize: 16, fontWeight: "600", }, infoRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingVertical: 5, borderBottomWidth: 1, borderBottomColor: "#F0F0F0", }, infoRowLast: { borderBottomWidth: 0, }, infoLabel: { color: "#666666", fontSize: 13, flexShrink: 0, marginRight: 12, }, infoValue: { color: "#111111", fontSize: 14, fontWeight: "600", }, firmwareRightWrap: { flexDirection: "row", alignItems: "center", justifyContent: "flex-end", flexShrink: 1, }, inlineDfuButton: { marginLeft: 10, backgroundColor: "#D9D9D9", borderRadius: 8, paddingHorizontal: 10, paddingVertical: 5, }, inlineDfuButtonHot: { backgroundColor: "#E7141E", }, inlineDfuButtonDisabled: { opacity: 0.6, }, inlineDfuButtonText: { color: "#333333", fontSize: 12, fontWeight: "600", }, inlineDfuButtonTextHot: { color: "#FFFFFF", }, connectedText: { color: "#0A9F4B", }, disconnectedText: { color: "#D43C3C", }, });