import React, { useState, useEffect, useMemo } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Modal, ActivityIndicator, useWindowDimensions, Platform, useColorScheme, } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { useConnectionStore } from '../../src/stores/connectionStore'; import { useDeviceStore } from '../../src/stores/deviceStore'; import { useDataStore } from '../../src/stores/dataStore'; import { useDevice } from '../../src/hooks/useDevice'; import { useWaveform, type WaveformData } from '../../src/hooks/useWaveform'; import { WaveformChart } from '../../src/components/WaveformChart'; import { ParamForm } from '../../src/components/ParamForm'; import { NoProjectGate } from '../../src/components/NoProjectGate'; import { CHANNEL_COLORS } from '../../src/protocol/constants'; import { formatUV, formatUtc, formatCoord, sourceModeLabel, formatBattery, formatTemperature } from '../../src/utils/format'; import { useTheme } from '../../src/design/tokens'; import { Spacing, Radius } from '../../src/design/tokens'; import * as ScreenOrientation from 'expo-screen-orientation'; import type { MeasurementFrame } from '../../src/protocol/types'; // ── Param sheet ─────────────────────────────────────────────────────────────── function ParamSheet({ visible, onClose, onSetup, busy, configDirty, }: { visible: boolean; onClose: () => void; onSetup: () => Promise; busy: boolean; configDirty: boolean; }) { const theme = useTheme(); const { deviceStatus } = useDeviceStore(); const measuring = deviceStatus !== 'idle'; return ( 采集参数 {configDirty && ( 待下发 )} {busy ? : {measuring ? '采集中,不可下发' : '下 发 配 置'} } ); } const PS = StyleSheet.create({ backdrop: { ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0,0,0,0.6)' }, sheet: { position: 'absolute', bottom: 0, left: 0, right: 0, maxHeight: '85%', borderTopLeftRadius: Radius.xl, borderTopRightRadius: Radius.xl, borderTopWidth: 1, paddingBottom: Spacing.xxl, }, handle: { width: 36, height: 4, borderRadius: 2, alignSelf: 'center', marginTop: Spacing.sm, marginBottom: Spacing.sm, }, header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: Spacing.lg, paddingBottom: Spacing.sm, gap: Spacing.sm }, title: { fontSize: 14, fontWeight: '700', flex: 1 }, dirtyBadge: { borderRadius: Radius.sm, paddingHorizontal: 7, paddingVertical: 2, borderWidth: 1 }, dirtyTxt: { fontSize: 9, fontWeight: '700' }, scroll: { flex: 1 }, setupBtn: { margin: Spacing.lg, borderRadius: Radius.lg, paddingVertical: 14, alignItems: 'center', borderWidth: 1, }, setupBtnDis: { opacity: 0.4 }, setupBtnTxt: { fontWeight: '700', fontSize: 14, letterSpacing: 3 }, }); // ── Control row ─────────────────────────────────────────────────────────────── function ControlRow({ deviceStatus, busy, frameId, onContinuous, onSingle, onStop, onSettings, configDirty, }: { deviceStatus: 'idle' | 'running' | 'single'; busy: boolean; frameId: number | undefined; onContinuous: () => void; onSingle: () => void; onStop: () => void; onSettings: () => void; configDirty: boolean; }) { const theme = useTheme(); const idle = deviceStatus === 'idle'; const running = deviceStatus === 'running'; const single = deviceStatus === 'single'; return ( {/* Primary action */} {idle && ( {busy ? : <> 连 续 采 集 } )} {running && ( 停 止 )} {single && ( 单次采集中… )} {/* Secondary: single-shot button (idle only) */} {idle && ( 单次 )} {/* Running indicator (running only) */} {running && frameId !== undefined && ( #{frameId} )} {/* Settings button — always present */} {configDirty && ( )} ); } const CR = StyleSheet.create({ row: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, gap: Spacing.sm, borderTopWidth: 1, }, primary: { flex: 2, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, paddingVertical: 14, borderRadius: Radius.lg, borderWidth: 1, }, dis: { opacity: 0.35 }, primaryIcon: { fontSize: 11 }, primaryTxt: { fontSize: 13, fontWeight: '700', letterSpacing: 1.5 }, secondary: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 5, paddingVertical: 14, borderRadius: Radius.lg, borderWidth: 1, }, secondaryIcon:{ fontSize: 11 }, secondaryTxt: { fontSize: 12, fontWeight: '700' }, indicator: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, paddingVertical: 14, borderRadius: Radius.lg, borderWidth: 1, }, runDot: { width: 6, height: 6, borderRadius: 3 }, indicatorTxt: { fontSize: 12, fontWeight: '700', fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' }, settingsBtn: { width: 44, height: 44, borderRadius: Radius.md, borderWidth: 1, alignItems: 'center', justifyContent: 'center', }, settingsTxt: { fontSize: 18 }, dirtyDot: { position: 'absolute', top: 6, right: 6, width: 7, height: 7, borderRadius: 4, borderWidth: 1, }, }); // ── Status panel ───────────────────────────────────────────────────────────── const CURRENT_RATIO = 50; const INT32_MAX = 0x7fffffff; function formatCurrent(raw: number): string { const amps = (raw / INT32_MAX) * 5.0 * CURRENT_RATIO; return `${amps.toFixed(1)} A`; } function StatusPanel({ frame }: { frame: MeasurementFrame }) { const theme = useTheme(); const m = frame.meta; const mono = { fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' } as const; const row1 = [ { label: '源模式', value: sourceModeLabel(m.sourceMode) }, { label: '发射电流', value: formatCurrent(m.current) }, { label: '电池', value: formatBattery(m.batteryVolt) }, { label: '温度', value: formatTemperature(m.temperature) }, ]; const row2 = [ { label: 'Roll', value: `${m.roll.toFixed(1)}°` }, { label: 'Pitch', value: `${m.pitch.toFixed(1)}°` }, { label: 'Yaw', value: `${m.yaw.toFixed(1)}°` }, { label: 'GPS', value: ['未定位','非差分','差分','无效PPS','固定解','浮点解','估算中'][m.gpsStatus] ?? '未知', highlight: m.gpsStatus > 0 }, { label: 'SD', value: ['正常','未插卡','异常'][m.sdStatus] ?? '未知', highlight: m.sdStatus === 0 }, ]; const renderCell = (it: { label: string; value: string; highlight?: boolean }) => ( {it.label} {it.value} ); return ( {row1.map(renderCell)} {row2.map(renderCell)} ); } const SP = StyleSheet.create({ container: { paddingHorizontal: Spacing.md, paddingVertical: 4, gap: 4, borderTopWidth: StyleSheet.hairlineWidth }, row: { flexDirection: 'row', gap: 4 }, cell: { flex: 1, alignItems: 'center', paddingVertical: 3, borderRadius: Radius.sm, borderWidth: 1 }, label: { fontSize: 8, fontWeight: '600' }, value: { fontSize: 10, fontWeight: '700', marginTop: 1 }, }); // ── Frame summary bar ───────────────────────────────────────────────────────── function FrameSummaryBar({ frame }: { frame: MeasurementFrame }) { const theme = useTheme(); return ( #{frame.frameId} {' '}{frame.meta ? formatUtc(frame.meta.utc) : ''} {frame.meta?.latitude ? ` ${formatCoord(frame.meta.latitude, false)} ${formatCoord(frame.meta.longitude, true)}` : ''} {frame.meta ? ` R${frame.meta.roll.toFixed(1)} P${frame.meta.pitch.toFixed(1)} Y${frame.meta.yaw.toFixed(1)}` : ''} ); } const FS = StyleSheet.create({ bar: { paddingHorizontal: Spacing.md, paddingVertical: 5, borderTopWidth: StyleSheet.hairlineWidth }, txt: { fontSize: 9, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' }, }); // ── Not connected placeholder ───────────────────────────────────────────────── function NotConnected({ onConnect }: { onConnect: () => void }) { const theme = useTheme(); return ( 未连接仪器 请先连接 TEM 接收机 连 接 设 备 ); } const NC = StyleSheet.create({ root: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 }, icon: { fontSize: 48 }, title: { fontSize: 17, fontWeight: '700' }, sub: { fontSize: 12 }, btn: { marginTop: Spacing.sm, borderRadius: Radius.lg, paddingVertical: 13, paddingHorizontal: 32, borderWidth: 1, }, btnTxt: { fontSize: 14, fontWeight: '700', letterSpacing: 2 }, }); // ── Main screen ─────────────────────────────────────────────────────────────── export default function WaveScreen() { const theme = useTheme(); const { width: sw, height: sh } = useWindowDimensions(); const { status, showModal } = useConnectionStore(); const { config, configDirty } = useDeviceStore(); const { deviceStatus } = useDeviceStore(); const { busy, setup, startContinuous, startSingle, stop } = useDevice(); const frame = useDataStore((s) => s.currentFrame); const sessionId = useDataStore((s) => s.sessionId); const projectId = useDataStore((s) => s.projectId); const hasProject = useDataStore((s) => s.hasProject); const [visible, setVisible] = useState(() => Array(6).fill(true)); const [logScale, setLogScale] = useState(false); const [sheetVisible, setSheetVisible] = useState(false); const [fullscreen, setFullscreen] = useState(false); const [projectName, setProjectName] = useState(null); useEffect(() => { if (fullscreen) { ScreenOrientation.unlockAsync(); } else { ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); } return () => { ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); }; }, [fullscreen]); const connected = status === 'connected'; const channelNum = config.channelNum; const CHART_H = Math.round(Math.min(280, sh * 0.36)); const data = useWaveform(visible); useEffect(() => { if (!projectId) { setProjectName(null); return; } import('../../src/services/StorageService').then(({ listProjects }) => listProjects().then((ps) => setProjectName(ps.find((p) => p.projectId === projectId)?.name ?? null)), ).catch(() => setProjectName(null)); }, [projectId]); const toggleChannel = (idx: number) => setVisible((prev) => prev.map((v, i) => (i === idx ? !v : v))); const handleSetup = async () => { await setup(); setSheetVisible(false); }; if (!connected) { return ( ); } if (!hasProject) { return ( ); } return ( {/* ── Context bar: project · session ── */} {projectName ? {projectName} · {sessionId} : {sessionId}} {/* ── Channel selector + LOG/LIN ── */} {Array.from({ length: channelNum }, (_, i) => ( toggleChannel(i)} activeOpacity={0.7} > CH{i + 1} ))} setLogScale((v) => !v)} activeOpacity={0.7} > {logScale ? 'LOG' : 'LIN'} setFullscreen(true)} activeOpacity={0.7} > {/* ── Waveform chart ── */} {data ? ( ) : ( 等待采样数据… )} {/* ── Peak values ── */} {/* ── Frame summary ── */} {frame && } {frame && } {/* ── Control row ── */} setSheetVisible(true)} configDirty={configDirty} /> {/* ── Param sheet ── */} setSheetVisible(false)} onSetup={handleSetup} busy={busy} configDirty={configDirty} /> {/* ── Fullscreen waveform ── */} setFullscreen(false)} > setLogScale((v) => !v)} onClose={() => setFullscreen(false)} /> ); } function FullscreenContent({ data, visibleChannels, logScale, onToggleLog, onClose, }: { data: WaveformData | null; visibleChannels: boolean[]; logScale: boolean; onToggleLog: () => void; onClose: () => void; }) { const theme = useTheme(); const scheme = useColorScheme(); const isDark = scheme !== 'light'; const { width: fsW, height: fsH } = useWindowDimensions(); return ( {data ? ( ) : ( 等待采样数据… )} {/* Floating toolbar */} {logScale ? 'LOG' : 'LIN'} ); } const S = StyleSheet.create({ root: { flex: 1 }, ctxBar: { paddingHorizontal: Spacing.md, paddingVertical: 4, borderBottomWidth: 1, }, ctxTxt: { fontSize: 9, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace', }, toolbar: { flexDirection: 'row', alignItems: 'center', paddingRight: Spacing.sm, borderBottomWidth: 1, }, chRow: { flexDirection: 'row', paddingHorizontal: Spacing.sm, paddingVertical: 6, gap: 5 }, chBtn: { flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: 8, paddingVertical: 5, borderRadius: Radius.md, borderWidth: 1, }, chDot: { width: 6, height: 6, borderRadius: 3 }, chTxt: { fontSize: 10, fontWeight: '600' }, logBtn: { paddingHorizontal: 9, paddingVertical: 5, borderRadius: Radius.sm, borderWidth: 1, marginLeft: 4, }, logTxt: { fontSize: 10, fontWeight: '700', letterSpacing: 0.5 }, fullBtn: { paddingHorizontal: 8, paddingVertical: 5, borderRadius: Radius.sm, borderWidth: 1, marginLeft: 4, }, fullTxt: { fontSize: 12 }, fsRoot: { flex: 1 }, fsToolbar: { position: 'absolute', top: 40, right: 12, flexDirection: 'row', gap: 8, }, fsCloseBtn: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', }, fsCloseTxt: { fontSize: 16, fontWeight: '700' }, chartWrap: { borderBottomWidth: StyleSheet.hairlineWidth }, chartPlaceholder: { justifyContent: 'center', alignItems: 'center', }, placeholderTxt: { fontSize: 13 }, });