- 将 TEMF v1 格式替换为设备 0x85 数据帧格式(10 字节帧头 + 54 字节元数据 + ADC 数据) - 重写 encodeBin/decodeBin 函数以支持交织排列的 ADC 采样数据 - 更新记录页面使用设备配置中的 sampleFreqCode 替代从二进制文件解析的值 - 在导入项目时从设备配置获取 accNum 以适配新格式
566 lines
20 KiB
TypeScript
566 lines
20 KiB
TypeScript
import * as SQLite from 'expo-sqlite';
|
|
import * as FileSystem from 'expo-file-system/legacy';
|
|
import type { MeasurementFrame, MeasurementMeta, SetupConfig, GateConfig } from '../protocol/types';
|
|
import { AMP_GAIN } from '../protocol/constants';
|
|
import * as BinLoader from './BinLoader';
|
|
import * as TemBundle from './TemBundle';
|
|
|
|
const DATA_DIR = (FileSystem.documentDirectory ?? '') + 'tem_data/';
|
|
|
|
// ── Public types ────────────────────────────────────────────────────────────
|
|
|
|
export interface ProjectInfo {
|
|
projectId: string;
|
|
name: string;
|
|
createdAt: number;
|
|
lineCount: number;
|
|
}
|
|
|
|
export interface SessionInfo {
|
|
sessionId: string;
|
|
createdAt: number;
|
|
frameCount: number;
|
|
projectId: string | null;
|
|
}
|
|
|
|
export interface PersistedFrame {
|
|
frameId: number;
|
|
sessionId: string;
|
|
timestamp: number;
|
|
accNum: number;
|
|
gain: number;
|
|
binPath: string | null;
|
|
meta: MeasurementMeta;
|
|
}
|
|
|
|
// ── DB init ─────────────────────────────────────────────────────────────────
|
|
|
|
let _db: SQLite.SQLiteDatabase | null = null;
|
|
|
|
async function getDb(): Promise<SQLite.SQLiteDatabase> {
|
|
if (!_db) {
|
|
_db = await SQLite.openDatabaseAsync('tem.db');
|
|
await _db.execAsync(`
|
|
PRAGMA journal_mode = WAL;
|
|
|
|
CREATE TABLE IF NOT EXISTS projects (
|
|
project_id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
session_id TEXT PRIMARY KEY,
|
|
created_at INTEGER NOT NULL,
|
|
frame_count INTEGER NOT NULL DEFAULT 0,
|
|
project_id TEXT REFERENCES projects(project_id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS frames (
|
|
frame_id INTEGER NOT NULL,
|
|
session_id TEXT NOT NULL,
|
|
ts INTEGER,
|
|
utc INTEGER, lon REAL, lat REAL, alt REAL,
|
|
ch_num INTEGER, acc_num INTEGER, gain REAL,
|
|
gps_st INTEGER, sd_st INTEGER, amp_ratio INTEGER,
|
|
current INTEGER, temp INTEGER, batt INTEGER,
|
|
roll REAL, pitch REAL, yaw REAL, src_mode INTEGER,
|
|
bin_path TEXT,
|
|
PRIMARY KEY (frame_id, session_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_fs ON frames(session_id);
|
|
`);
|
|
await runMigrations(_db);
|
|
}
|
|
return _db;
|
|
}
|
|
|
|
async function runMigrations(db: SQLite.SQLiteDatabase) {
|
|
const row = await db.getFirstAsync<{ user_version: number }>('PRAGMA user_version');
|
|
let version = row?.user_version ?? 0;
|
|
|
|
if (version < 1) {
|
|
try { await db.runAsync('ALTER TABLE sessions ADD COLUMN project_id TEXT'); } catch {}
|
|
await db.runAsync('PRAGMA user_version = 1');
|
|
version = 1;
|
|
}
|
|
if (version < 2) {
|
|
const orphans = await db.getAllAsync<{ session_id: string }>(
|
|
'SELECT session_id FROM sessions WHERE project_id IS NULL',
|
|
);
|
|
if (orphans.length > 0) {
|
|
const defId = generateProjectId();
|
|
await db.runAsync(
|
|
'INSERT INTO projects (project_id, name, created_at) VALUES (?,?,?)',
|
|
[defId, '默认工程', Date.now()],
|
|
);
|
|
await db.runAsync('UPDATE sessions SET project_id = ? WHERE project_id IS NULL', [defId]);
|
|
}
|
|
await db.runAsync('PRAGMA user_version = 2');
|
|
version = 2;
|
|
}
|
|
}
|
|
|
|
async function ensureDataDir() {
|
|
const info = await FileSystem.getInfoAsync(DATA_DIR);
|
|
if (!info.exists) await FileSystem.makeDirectoryAsync(DATA_DIR, { intermediates: true });
|
|
}
|
|
|
|
export async function initStorage(): Promise<void> {
|
|
await getDb();
|
|
await ensureDataDir();
|
|
}
|
|
|
|
export async function hasAnyProject(): Promise<boolean> {
|
|
const d = await getDb();
|
|
const row = await d.getFirstAsync<{ cnt: number }>('SELECT COUNT(*) AS cnt FROM projects');
|
|
return (row?.cnt ?? 0) > 0;
|
|
}
|
|
|
|
export async function getFirstProject(): Promise<ProjectInfo | null> {
|
|
const d = await getDb();
|
|
const r = await d.getFirstAsync<{ project_id: string; name: string; created_at: number }>(
|
|
'SELECT project_id, name, created_at FROM projects ORDER BY created_at DESC LIMIT 1',
|
|
);
|
|
if (!r) return null;
|
|
return { projectId: r.project_id, name: r.name, createdAt: r.created_at, lineCount: 0 };
|
|
}
|
|
|
|
// ── Project CRUD ────────────────────────────────────────────────────────────
|
|
|
|
function generateProjectId(): string {
|
|
const now = new Date();
|
|
const pad = (n: number, d = 2) => String(n).padStart(d, '0');
|
|
const rand = Math.random().toString(36).slice(2, 6);
|
|
return (
|
|
`P${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +
|
|
`${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` +
|
|
`${pad(now.getMilliseconds(), 3)}_${rand}`
|
|
);
|
|
}
|
|
|
|
function sanitizeName(name: string): string {
|
|
return name.trim().replace(/[\/\\:*?"<>|.\x00]/g, '_').slice(0, 50);
|
|
}
|
|
|
|
export async function createProject(name: string): Promise<string> {
|
|
const d = await getDb();
|
|
const id = generateProjectId();
|
|
await d.runAsync(
|
|
'INSERT INTO projects (project_id, name, created_at) VALUES (?,?,?)',
|
|
[id, sanitizeName(name), Date.now()],
|
|
);
|
|
return id;
|
|
}
|
|
|
|
export async function listProjects(): Promise<ProjectInfo[]> {
|
|
const d = await getDb();
|
|
const rows = await d.getAllAsync<{
|
|
project_id: string; name: string; created_at: number; line_count: number;
|
|
}>(
|
|
`SELECT p.project_id, p.name, p.created_at,
|
|
COUNT(s.session_id) AS line_count
|
|
FROM projects p
|
|
LEFT JOIN sessions s ON s.project_id = p.project_id
|
|
GROUP BY p.project_id
|
|
ORDER BY p.created_at DESC`,
|
|
);
|
|
return rows.map(r => ({
|
|
projectId: r.project_id,
|
|
name: r.name,
|
|
createdAt: r.created_at,
|
|
lineCount: r.line_count,
|
|
}));
|
|
}
|
|
|
|
export async function renameProject(projectId: string, name: string): Promise<void> {
|
|
const d = await getDb();
|
|
await d.runAsync('UPDATE projects SET name = ? WHERE project_id = ?', [name.trim(), projectId]);
|
|
}
|
|
|
|
export async function deleteProject(projectId: string): Promise<void> {
|
|
const d = await getDb();
|
|
const sessions = await d.getAllAsync<{ session_id: string }>(
|
|
'SELECT session_id FROM sessions WHERE project_id = ?', [projectId],
|
|
);
|
|
for (const s of sessions) {
|
|
const frames = await d.getAllAsync<{ bin_path: string | null }>(
|
|
'SELECT bin_path FROM frames WHERE session_id = ?', [s.session_id],
|
|
);
|
|
for (const f of frames) {
|
|
if (f.bin_path) { try { await FileSystem.deleteAsync(f.bin_path, { idempotent: true }); } catch {} }
|
|
}
|
|
await d.runAsync('DELETE FROM frames WHERE session_id = ?', [s.session_id]);
|
|
}
|
|
await d.runAsync('DELETE FROM sessions WHERE project_id = ?', [projectId]);
|
|
await d.runAsync('DELETE FROM projects WHERE project_id = ?', [projectId]);
|
|
}
|
|
|
|
// ── Session CRUD ────────────────────────────────────────────────────────────
|
|
|
|
export async function ensureSession(sessionId: string, projectId?: string | null): Promise<void> {
|
|
const d = await getDb();
|
|
await d.runAsync(
|
|
'INSERT OR IGNORE INTO sessions (session_id, created_at, frame_count, project_id) VALUES (?,?,0,?)',
|
|
[sessionId, Date.now(), projectId ?? null],
|
|
);
|
|
if (projectId) {
|
|
await d.runAsync(
|
|
'UPDATE sessions SET project_id = ? WHERE session_id = ? AND project_id IS NULL',
|
|
[projectId, sessionId],
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function listSessions(): Promise<SessionInfo[]> {
|
|
const d = await getDb();
|
|
const rows = await d.getAllAsync<{
|
|
session_id: string; created_at: number; frame_count: number; project_id: string | null;
|
|
}>(
|
|
'SELECT session_id, created_at, frame_count, project_id FROM sessions ORDER BY created_at DESC',
|
|
);
|
|
return rows.map(r => ({
|
|
sessionId: r.session_id,
|
|
createdAt: r.created_at,
|
|
frameCount: r.frame_count,
|
|
projectId: r.project_id,
|
|
}));
|
|
}
|
|
|
|
export async function listSessionsByProject(projectId: string): Promise<SessionInfo[]> {
|
|
const d = await getDb();
|
|
const rows = await d.getAllAsync<{
|
|
session_id: string; created_at: number; frame_count: number;
|
|
}>(
|
|
'SELECT session_id, created_at, frame_count FROM sessions WHERE project_id = ? ORDER BY created_at DESC',
|
|
[projectId],
|
|
);
|
|
return rows.map(r => ({
|
|
sessionId: r.session_id,
|
|
createdAt: r.created_at,
|
|
frameCount: r.frame_count,
|
|
projectId,
|
|
}));
|
|
}
|
|
|
|
export async function assignSessionToProject(sessionId: string, projectId: string | null): Promise<void> {
|
|
const d = await getDb();
|
|
await d.runAsync('UPDATE sessions SET project_id = ? WHERE session_id = ?', [projectId, sessionId]);
|
|
}
|
|
|
|
export async function deleteSession(sessionId: string): Promise<void> {
|
|
const d = await getDb();
|
|
try {
|
|
const files = await FileSystem.readDirectoryAsync(DATA_DIR);
|
|
await Promise.all(
|
|
files
|
|
.filter(f => f.startsWith(sessionId))
|
|
.map(f => FileSystem.deleteAsync(DATA_DIR + f, { idempotent: true })),
|
|
);
|
|
} catch { /* directory may not exist */ }
|
|
await d.runAsync('DELETE FROM frames WHERE session_id = ?', [sessionId]);
|
|
await d.runAsync('DELETE FROM sessions WHERE session_id = ?', [sessionId]);
|
|
}
|
|
|
|
// ── Frame persistence ────────────────────────────────────────────────────────
|
|
|
|
export async function persistFrame(frame: MeasurementFrame, sessionId: string): Promise<void> {
|
|
const d = await getDb();
|
|
const m = frame.meta;
|
|
|
|
let binPath: string | null = null;
|
|
try {
|
|
const fname = `${sessionId}_f${String(frame.frameId).padStart(6, '0')}.bin`;
|
|
binPath = DATA_DIR + fname;
|
|
await BinLoader.saveBinFile(binPath, frame);
|
|
} catch { binPath = null; }
|
|
|
|
await d.runAsync(
|
|
`INSERT OR IGNORE INTO frames (
|
|
frame_id, session_id, ts, utc, lon, lat, alt,
|
|
ch_num, acc_num, gain, gps_st, sd_st, amp_ratio,
|
|
current, temp, batt, roll, pitch, yaw, src_mode, bin_path
|
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
[
|
|
frame.frameId, sessionId, frame.timestamp,
|
|
m.utc, m.longitude, m.latitude, m.altitude,
|
|
m.channelNum, frame.accNum, frame.gain,
|
|
m.gpsStatus, m.sdStatus, m.ampRatio,
|
|
m.current, m.temperature, m.batteryVolt,
|
|
m.roll, m.pitch, m.yaw, m.sourceMode, binPath,
|
|
],
|
|
);
|
|
|
|
await d.runAsync(
|
|
'UPDATE sessions SET frame_count = frame_count + 1 WHERE session_id = ?',
|
|
[sessionId],
|
|
);
|
|
}
|
|
|
|
export async function loadSessionFrames(sessionId: string): Promise<PersistedFrame[]> {
|
|
const d = await getDb();
|
|
const rows = await d.getAllAsync<any>(
|
|
'SELECT * FROM frames WHERE session_id = ? ORDER BY frame_id DESC',
|
|
[sessionId],
|
|
);
|
|
return rows.map(r => ({
|
|
frameId: r.frame_id,
|
|
sessionId: r.session_id,
|
|
timestamp: r.ts,
|
|
accNum: r.acc_num,
|
|
gain: r.gain,
|
|
binPath: r.bin_path,
|
|
meta: {
|
|
devId: 0, utc: r.utc,
|
|
longitude: r.lon, latitude: r.lat, altitude: r.alt, height: 0,
|
|
sdStatus: r.sd_st, gpsStatus: r.gps_st, ampRatio: r.amp_ratio,
|
|
roll: r.roll, pitch: r.pitch, yaw: r.yaw,
|
|
channelNum: r.ch_num, current: r.current,
|
|
temperature: r.temp, batteryVolt: r.batt, sourceMode: r.src_mode,
|
|
} as MeasurementMeta,
|
|
}));
|
|
}
|
|
|
|
export async function deleteFrame(frameId: number, sessionId: string): Promise<void> {
|
|
const d = await getDb();
|
|
const row = await d.getFirstAsync<{ bin_path: string | null }>(
|
|
'SELECT bin_path FROM frames WHERE frame_id = ? AND session_id = ?',
|
|
[frameId, sessionId],
|
|
);
|
|
if (row?.bin_path) {
|
|
try { await FileSystem.deleteAsync(row.bin_path, { idempotent: true }); } catch { /* ignore */ }
|
|
}
|
|
await d.runAsync('DELETE FROM frames WHERE frame_id = ? AND session_id = ?', [frameId, sessionId]);
|
|
await d.runAsync('UPDATE sessions SET frame_count = MAX(0, frame_count - 1) WHERE session_id = ?', [sessionId]);
|
|
}
|
|
|
|
export async function getMaxFrameId(sessionId: string): Promise<number> {
|
|
const d = await getDb();
|
|
const row = await d.getFirstAsync<{ max_id: number | null }>(
|
|
'SELECT MAX(frame_id) AS max_id FROM frames WHERE session_id = ?',
|
|
[sessionId],
|
|
);
|
|
return row?.max_id ?? -1;
|
|
}
|
|
|
|
// ── CSV export ──────────────────────────────────────────────────────────────
|
|
|
|
export async function exportSessionMetaCsv(sessionId: string): Promise<string> {
|
|
const frames = await loadSessionFrames(sessionId);
|
|
if (frames.length === 0) throw new Error('会话无数据');
|
|
|
|
const { formatUtc } = await import('../utils/format');
|
|
|
|
const maxCh = frames.reduce((m, f) => Math.max(m, f.meta.channelNum), 0);
|
|
const chHeaders = Array.from({ length: maxCh }, (_, 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: string[] = [];
|
|
for (const f of frames) {
|
|
const m = f.meta;
|
|
let peaks: string[] = Array(maxCh).fill('');
|
|
if (f.binPath) {
|
|
const bin = await BinLoader.loadBinFile(f.binPath);
|
|
if (bin) {
|
|
peaks = Array.from({ length: maxCh }, (_, i) => {
|
|
const ch = bin.adcUV[i];
|
|
if (!ch) return '';
|
|
const pk = ch.reduce((mx, v) => Math.max(mx, Math.abs(v)), 0);
|
|
return pk.toFixed(4);
|
|
});
|
|
}
|
|
}
|
|
rows.push([
|
|
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(','));
|
|
}
|
|
|
|
const csv = [header, ...rows].join('\n');
|
|
await ensureDataDir();
|
|
const path = DATA_DIR + `TEM_${sessionId}.csv`;
|
|
await FileSystem.writeAsStringAsync(path, csv, { encoding: FileSystem.EncodingType.UTF8 });
|
|
return path;
|
|
}
|
|
|
|
// ── .tem export ─────────────────────────────────────────────────────────────
|
|
|
|
export async function exportProject(
|
|
projectId: string,
|
|
deviceConfig: SetupConfig,
|
|
gateConfig: GateConfig,
|
|
onProgress?: (current: number, total: number) => void,
|
|
): Promise<string> {
|
|
const d = await getDb();
|
|
|
|
const proj = await d.getFirstAsync<{ name: string; created_at: number }>(
|
|
'SELECT name, created_at FROM projects WHERE project_id = ?',
|
|
[projectId],
|
|
);
|
|
if (!proj) throw new Error('工程不存在');
|
|
|
|
const sessions = await listSessionsByProject(projectId);
|
|
|
|
// Collect all frames + bin bytes
|
|
const bins: Record<string, Uint8Array> = {};
|
|
const sessionManifests: TemBundle.SessionManifest[] = [];
|
|
let totalFrames = sessions.reduce((s, x) => s + x.frameCount, 0);
|
|
let done = 0;
|
|
|
|
for (const session of sessions) {
|
|
const frames = await loadSessionFrames(session.sessionId);
|
|
for (const f of frames) {
|
|
if (f.binPath) {
|
|
const bytes = await BinLoader.readFileBytes(f.binPath);
|
|
if (bytes) {
|
|
const relPath = TemBundle.bundleBinPath(session.sessionId, f.frameId);
|
|
bins[relPath] = bytes;
|
|
}
|
|
}
|
|
onProgress?.(++done, totalFrames);
|
|
}
|
|
sessionManifests.push({
|
|
id: session.sessionId,
|
|
created_at: session.createdAt,
|
|
frame_count: session.frameCount,
|
|
device_config: deviceConfig,
|
|
});
|
|
}
|
|
|
|
const manifest: TemBundle.ProjectManifest = {
|
|
schema_version: '1.0',
|
|
exported_at: new Date().toISOString(),
|
|
project: { id: projectId, name: proj.name, created_at: proj.created_at },
|
|
device_config: deviceConfig,
|
|
gate_config: gateConfig,
|
|
sessions: sessionManifests,
|
|
};
|
|
|
|
const bundleBytes = await TemBundle.packBundle(manifest, bins);
|
|
|
|
await ensureDataDir();
|
|
const safeName = sanitizeName(proj.name);
|
|
const ts = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
const outPath = DATA_DIR + `TEM_${safeName}_${ts}.tem`;
|
|
await BinLoader.writeFileBytes(outPath, bundleBytes);
|
|
return outPath;
|
|
}
|
|
|
|
// ── .tem import ─────────────────────────────────────────────────────────────
|
|
|
|
export interface ImportResult {
|
|
projectId: string;
|
|
projectName: string;
|
|
sessionCount: number;
|
|
frameCount: number;
|
|
}
|
|
|
|
export async function importProject(
|
|
bundleUri: string,
|
|
onProgress?: (current: number, total: number) => void,
|
|
): Promise<ImportResult> {
|
|
await ensureDataDir();
|
|
const d = await getDb();
|
|
|
|
// Copy to temp path (handles content:// URIs on Android)
|
|
const tmpPath = DATA_DIR + `tmp_import_${Date.now()}.tem`;
|
|
try {
|
|
await FileSystem.copyAsync({ from: bundleUri, to: tmpPath });
|
|
} catch {
|
|
// URI may already be a file:// path — try reading directly
|
|
}
|
|
|
|
let bundleBytes: Uint8Array | null = null;
|
|
try {
|
|
bundleBytes = await BinLoader.readFileBytes(tmpPath);
|
|
} catch { /* fall through */ }
|
|
|
|
if (!bundleBytes) {
|
|
bundleBytes = await BinLoader.readFileBytes(bundleUri);
|
|
}
|
|
if (!bundleBytes) throw new Error('无法读取 .tem 文件');
|
|
|
|
// Clean up temp file (don't await — non-critical)
|
|
FileSystem.deleteAsync(tmpPath, { idempotent: true }).catch(() => {});
|
|
|
|
const { manifest, bins } = await TemBundle.parseBundle(bundleBytes);
|
|
|
|
// Allocate new project ID to avoid collision
|
|
const newProjectId = generateProjectId();
|
|
await d.runAsync(
|
|
'INSERT INTO projects (project_id, name, created_at) VALUES (?,?,?)',
|
|
[newProjectId, manifest.project.name, manifest.project.created_at],
|
|
);
|
|
|
|
const totalFrames = Object.keys(bins).length;
|
|
let done = 0;
|
|
let importedFrames = 0;
|
|
|
|
for (const session of manifest.sessions) {
|
|
// Use original session ID if not already in DB; otherwise suffix with timestamp
|
|
const existing = await d.getFirstAsync<{ session_id: string }>(
|
|
'SELECT session_id FROM sessions WHERE session_id = ?',
|
|
[session.id],
|
|
);
|
|
const newSessionId = existing ? `${session.id}_${Date.now()}` : session.id;
|
|
|
|
await d.runAsync(
|
|
'INSERT INTO sessions (session_id, created_at, frame_count, project_id) VALUES (?,?,0,?)',
|
|
[newSessionId, session.created_at, newProjectId],
|
|
);
|
|
|
|
let sessionFrameCount = 0;
|
|
for (const [relPath, binBytes] of Object.entries(bins)) {
|
|
const parsed = TemBundle.parseBinPath(relPath);
|
|
if (!parsed || parsed.sessionId !== session.id) continue;
|
|
|
|
const { frameId } = parsed;
|
|
const meta = BinLoader.decodeBin(binBytes);
|
|
if (!meta) { onProgress?.(++done, totalFrames); continue; }
|
|
|
|
// accNum/sampleFreq not in device frame — get from manifest config
|
|
const cfg = session.device_config ?? manifest.device_config;
|
|
const accNum = cfg.accNum ?? 1;
|
|
|
|
// Write bin file to DATA_DIR
|
|
const fname = `${newSessionId}_f${String(frameId).padStart(6, '0')}.bin`;
|
|
const binPath = DATA_DIR + fname;
|
|
await BinLoader.writeFileBytes(binPath, binBytes);
|
|
|
|
// Insert frame record
|
|
await d.runAsync(
|
|
`INSERT OR IGNORE INTO frames (
|
|
frame_id, session_id, ts, utc, lon, lat, alt,
|
|
ch_num, acc_num, gain, gps_st, sd_st, amp_ratio,
|
|
current, temp, batt, roll, pitch, yaw, src_mode, bin_path
|
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
[
|
|
frameId, newSessionId, meta.timestamp,
|
|
meta.utc, meta.longitude, meta.latitude, meta.altitude,
|
|
meta.channelNum, accNum, AMP_GAIN[meta.ampRatio] ?? 1,
|
|
meta.gpsStatus, meta.sdStatus, meta.ampRatio,
|
|
meta.current, meta.temperature, meta.batteryVolt,
|
|
meta.roll, meta.pitch, meta.yaw, meta.srcMode, binPath,
|
|
],
|
|
);
|
|
|
|
sessionFrameCount++;
|
|
importedFrames++;
|
|
onProgress?.(++done, totalFrames);
|
|
}
|
|
|
|
await d.runAsync('UPDATE sessions SET frame_count = ? WHERE session_id = ?', [sessionFrameCount, newSessionId]);
|
|
}
|
|
|
|
return {
|
|
projectId: newProjectId,
|
|
projectName: manifest.project.name,
|
|
sessionCount: manifest.sessions.length,
|
|
frameCount: importedFrames,
|
|
};
|
|
}
|