// Use legacy API for documentDirectory compatibility import * as FileSystem from 'expo-file-system/legacy'; import * as Sharing from 'expo-sharing'; import type { MeasurementFrame } from '../protocol/types'; import { formatUtc, formatCoord } from './format'; const DATA_DIR = (FileSystem.documentDirectory ?? '') + 'tem_data/'; async function ensureDir() { const info = await FileSystem.getInfoAsync(DATA_DIR); if (!info.exists) await FileSystem.makeDirectoryAsync(DATA_DIR, { intermediates: true }); } // Build CSV content for a list of frames function framesToCsv(frames: MeasurementFrame[]): string { if (frames.length === 0) return ''; const channelNum = frames[0].meta.channelNum; const chHeaders = Array.from({ length: channelNum }, (_, i) => `CH${i + 1}_peak_uV`).join(','); const header = `FrameID,UTC,Longitude,Latitude,Altitude,GPS,SD,AmpRatio,Current,Temp,Battery,Roll,Pitch,Yaw,${chHeaders}`; const rows = frames.map((f) => { const m = f.meta; const peaks = f.adcUV.map((ch) => { const maxAbs = ch.reduce((mx, v) => Math.max(mx, Math.abs(v)), 0); return maxAbs.toFixed(4); }); return [ f.frameId, formatUtc(m.utc), m.longitude.toFixed(7), m.latitude.toFixed(7), m.altitude.toFixed(2), m.gpsStatus, m.sdStatus, m.ampRatio, m.current, m.temperature, m.batteryVolt, m.roll.toFixed(2), m.pitch.toFixed(2), m.yaw.toFixed(2), ...peaks, ].join(','); }); return [header, ...rows].join('\n'); } export async function exportCsv(frames: MeasurementFrame[], sessionId: string): Promise { await ensureDir(); const filename = `TEM_${sessionId}.csv`; const path = DATA_DIR + filename; const csv = framesToCsv(frames); await FileSystem.writeAsStringAsync(path, csv, { encoding: FileSystem.EncodingType.UTF8 }); return path; } // Save a single frame's raw ADC data as binary file export async function saveFrameBin(frame: MeasurementFrame, sessionId: string): Promise { await ensureDir(); const filename = `${sessionId}_f${String(frame.frameId).padStart(6, '0')}.bin`; const path = DATA_DIR + filename; const channelNum = frame.adcRaw.length; const samplesPerCh = frame.adcRaw[0]?.length ?? 0; // Header: magic(4) + channelNum(4) + samplesPerCh(4) = 12 bytes const totalBytes = 12 + channelNum * samplesPerCh * 4; const buf = new ArrayBuffer(totalBytes); const view = new DataView(buf); // Magic: "TEM\x01" view.setUint8(0, 0x54); view.setUint8(1, 0x45); view.setUint8(2, 0x4d); view.setUint8(3, 0x01); view.setUint32(4, channelNum, true); view.setUint32(8, samplesPerCh, true); let offset = 12; for (const ch of frame.adcRaw) { for (let i = 0; i < ch.length; i++) { view.setInt32(offset, ch[i], true); offset += 4; } } // Convert ArrayBuffer to base64 string for expo-file-system const uint8 = new Uint8Array(buf); let binary = ''; for (let i = 0; i < uint8.length; i++) binary += String.fromCharCode(uint8[i]); const b64 = btoa(binary); await FileSystem.writeAsStringAsync(path, b64, { encoding: FileSystem.EncodingType.Base64 }); return path; } export async function shareFile(filePath: string) { const canShare = await Sharing.isAvailableAsync(); if (canShare) { await Sharing.shareAsync(filePath); } } export async function listSessionFiles(sessionId: string): Promise { await ensureDir(); const all = await FileSystem.readDirectoryAsync(DATA_DIR); return all.filter((f) => f.startsWith(sessionId)).map((f) => DATA_DIR + f); }