106 lines
3.5 KiB
TypeScript
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);
|
|
}
|