2026-06-06 22:13:00 +08:00

106 lines
3.5 KiB
TypeScript

// 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<string> {
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<string> {
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<string[]> {
await ensureDir();
const all = await FileSystem.readDirectoryAsync(DATA_DIR);
return all.filter((f) => f.startsWith(sessionId)).map((f) => DATA_DIR + f);
}