File upload. Yeah

This commit is contained in:
2026-03-17 15:02:34 +01:00
parent 6ec66cd9da
commit 574ab9fa30
19 changed files with 1479 additions and 250 deletions

View File

@@ -6,6 +6,7 @@ import { crc32 } from './crc32';
import { get } from 'svelte/store';
import { saveLocalFile } from '../db';
import { refreshLocal } from '../sync';
import { file } from 'astro:schema';
let lastUiUpdate = 0;
let currentFileCrc32 = 0;
@@ -17,7 +18,7 @@ let fileChunks: Uint8Array[] = [];
let lsTimeout: ReturnType<typeof setTimeout> | null = null;
let lsResolve: ((data: any[]) => void) | null = null;
let lsReject: ((error: Error) => void) | null = null;
let fileGetResolve: ((success: boolean) => void) | null = null;
let fileGetResolve: ((result: { success: boolean, blob?: Blob }) => void) | null = null;
let fileGetReject: ((error: Error) => void) | null = null;
export function showErrorToast(errorCode: number) {
@@ -87,7 +88,6 @@ export function parseIncomingFrame(view: DataView, sender: FrameSender) {
case FRAME.LS_END:
if (lsTimeout) clearTimeout(lsTimeout);
const total = view.getUint32(3, true);
console.debug(`LS Stream beendet. Erwartete Einträge: ${total}, empfangen: ${lsBuffer.length}`, lsBuffer);
if (total !== lsBuffer.length) {
console.warn(`LS Stream: Erwartete Anzahl Einträge laut Header: ${total}, tatsächlich empfangen: ${lsBuffer.length}`);
addToast(`Warnung: LS Stream erwartete ${total} Einträge, aber nur ${lsBuffer.length} empfangen.`, 'warning');
@@ -98,20 +98,22 @@ export function parseIncomingFrame(view: DataView, sender: FrameSender) {
}
break;
case FRAME.FILE_START:
case FRAME.FILE_START:
currentFileCrc32 = 0;
const totalBytes = view.getUint32(3, true);
const nowStart = performance.now();
fileChunks = [];
transferStats.update(s => ({
...s,
bytesTotal: totalBytes,
bytesDone: 0,
currentFileName: s.pendingFileName || s.currentFileName,
fileStartTime: nowStart,
bulkStartTime: s.bulkStartTime === 0 ? nowStart : s.bulkStartTime
}));
if (fileTransfer.mode === 'file') {
transferStats.update(s => ({
...s,
bytesTotal: totalBytes,
bytesDone: 0,
currentFileName: s.pendingFileName || s.currentFileName,
fileStartTime: nowStart,
bulkStartTime: s.bulkStartTime === 0 ? nowStart : s.bulkStartTime
}));
}
// Parser-interne Metriken (Watchdog etc.)
fileTransfer.totalBytes = totalBytes;
@@ -120,8 +122,6 @@ case FRAME.FILE_START:
fileTransfer.startTime = nowStart;
lastUiUpdate = 0;
console.log(`[FILE_GET] Stream gestartet. Erwartete Größe: ${fileTransfer.totalBytes} Bytes.`);
fileTransfer.metricsTimer = setInterval(() => {
if (!fileTransfer.active) return;
@@ -166,17 +166,18 @@ case FRAME.FILE_START:
fileTransfer.receivedBytes += payloadLength;
fileTransfer.credits--;
const nowChunk = performance.now();
if (nowChunk - lastUiUpdate > SETTINGS.ui.transferUpdateIntervalMs) {
const delta = fileTransfer.receivedBytes - previousReceived; // Das Delta seit dem letzten Paket
if (fileTransfer.mode === 'file') {
const nowChunk = performance.now();
if (nowChunk - lastUiUpdate > SETTINGS.ui.transferUpdateIntervalMs) {
const delta = fileTransfer.receivedBytes - previousReceived;
transferStats.update(s => ({
...s,
bytesDone: fileTransfer.receivedBytes,
overallDone: s.overallDone + (fileTransfer.receivedBytes - s.bytesDone)
}));
console.log("[FILE_GET] Fortschritt: " + ((fileTransfer.receivedBytes / fileTransfer.totalBytes) * 100).toFixed(2) + "%");
lastUiUpdate = nowChunk;
transferStats.update(s => ({
...s,
bytesDone: fileTransfer.receivedBytes,
overallDone: s.overallDone + (fileTransfer.receivedBytes - s.bytesDone)
}));
lastUiUpdate = nowChunk;
}
}
if (fileTransfer.credits <= 64) {
@@ -186,53 +187,74 @@ case FRAME.FILE_START:
break;
case FRAME.FILE_END:
transferStats.update(s => {
return {
if (fileTransfer.mode === 'file') {
transferStats.update(s => ({
...s,
bytesDone: s.bytesTotal,
};
});
// Watchdog stoppen
}));
}
if (fileTransfer.metricsTimer) clearInterval(fileTransfer.metricsTimer);
fileTransfer.active = false;
const buzzerCrc32 = view.getUint32(3, true);
console.log(`[CRC] Lokal: 0x${currentFileCrc32.toString(16).toUpperCase()}`);
console.log(`[CRC] Buzzer: 0x${buzzerCrc32.toString(16).toUpperCase()}`);
const totalElapsed = (performance.now() - fileTransfer.startTime) / 1000;
const avgSpeed = (fileTransfer.receivedBytes / 1024) / totalElapsed;
console.log(`[FILE_GET] Stream beendet.`);
console.log(`[FILE_GET] Empfangen: ${fileTransfer.receivedBytes} Bytes in ${totalElapsed.toFixed(2)}s.`);
console.log(`[FILE_GET] Durchschnitt: ${avgSpeed.toFixed(2)} KB/s`);
if (currentFileCrc32 === buzzerCrc32) {
console.log("%c[CRC] Match! Datei ist integer.", "color: green; font-weight: bold;");
const fileBlob = new Blob(fileChunks, { type: 'audio/wav' });
const fileName = get(transferStats).currentFileName;
saveLocalFile(fileName, fileBlob, fileTransfer.totalBytes)
.then(() => {
console.log(`Datei ${fileName} erfolgreich lokal gespeichert.`);
refreshLocal();
})
.catch(err => {
console.error("Datenbankfehler:", err);
addToast(`Speichern von ${fileName} fehlgeschlagen.`, 'error');
});
const fileBlob = new Blob(fileChunks, { type: 'application/octet-stream' });
if (fileTransfer.mode === 'file') {
const fileName = get(transferStats).currentFileName;
saveLocalFile(fileName, fileBlob, fileTransfer.totalBytes)
.then(() => {
refreshLocal();
if (fileGetResolve) {
fileGetResolve({ success: true });
}
})
.catch(err => {
console.error("Datenbankfehler:", err);
addToast(`Speichern von ${fileName} fehlgeschlagen.`, 'error');
if (fileGetReject) fileGetReject(err);
})
.finally(() => {
fileGetResolve = null;
fileGetReject = null;
});
} else {
// TAGS Modus: Blob direkt zurückgeben, nichts speichern
if (fileGetResolve) fileGetResolve({ success: true, blob: fileBlob });
fileGetResolve = null;
fileGetReject = null;
}
} else {
console.error("[CRC] Mismatch! Datei beschädigt.");
addToast("CRC Fehler: Datei wurde fehlerhaft übertragen.", "error");
if (fileGetReject) fileGetReject(new Error("CRC Mismatch"));
break;
}
if (fileGetResolve) {
fileGetResolve(true);
fileGetResolve = null;
fileGetReject = null;
}
break;
case FRAME.ACK:
if (uploadState.active && payloadLength >= 2) {
const creditsAdded = view.getUint16(3, true);
uploadState.credits += creditsAdded;
if (uploadState.onCreditsAdded) {
uploadState.onCreditsAdded();
}
}
case FRAME.SUCCESS:
if (payloadLength >= 1) {
const successDataType = view.getUint8(3);
if (uploadState.active && successDataType === DATA.FILE_PUT || successDataType === DATA.TAGS_PUT) {
if (uploadState.onSuccess) uploadState.onSuccess();
}
}
break;
case FRAME.ERROR:
const errorCode = view.getUint16(3, true);
console.error(`Received error frame with code: 0x${errorCode.toString(16)}`);
@@ -249,6 +271,9 @@ case FRAME.FILE_START:
fileGetResolve = null;
fileGetReject = null;
}
if (uploadState.active && uploadState.onError) {
uploadState.onError(new Error(`Buzzer Error 0x${errorCode.toString(16)}`));
}
break;
default:
@@ -304,7 +329,6 @@ async function sendCredits(count: number, send: FrameSender) {
const buffer = new ArrayBuffer(5);
const view = new DataView(buffer);
console.debug(`Sende ${count} Credits für Stream...`);
view.setUint8(0, FRAME.ACK);
view.setUint16(1, 2, true);
view.setUint16(3, count, true);
@@ -332,6 +356,7 @@ export function setLsResolver(resolve: (data: any[]) => void, reject: (error: Er
const fileTransfer = {
active: false,
mode: 'file' as 'file' | 'tags',
startTime: 0,
totalBytes: 0,
receivedBytes: 0,
@@ -341,9 +366,22 @@ const fileTransfer = {
metricsTimer: null as ReturnType<typeof setInterval> | null
};
export function setFileGetResolver(resolve: (success: boolean) => void, reject: (error: Error) => void) {
export const uploadState = {
active: false,
credits: 0,
onCreditsAdded: null as (() => void) | null,
onSuccess: null as (() => void) | null,
onError: null as ((err: Error) => void) | null,
};
export function setFileGetResolver(
resolve: (result: { success: boolean, blob?: Blob }) => void,
reject: (error: Error) => void,
mode: 'file' | 'tags' = 'file' // Standard ist 'file'
) {
fileGetResolve = resolve;
fileGetReject = reject;
fileTransfer.mode = mode;
}
export function buildFileGetRequest(path: string): ArrayBuffer {
@@ -359,5 +397,21 @@ export function buildFileGetRequest(path: string): ArrayBuffer {
const uint8Buffer = new Uint8Array(buffer);
uint8Buffer.set(pathBytes, 4);
return buffer;
}
export function buildTagsGetRequest(path: string): ArrayBuffer {
const encoder = new TextEncoder();
const pathBytes = encoder.encode(path);
const buffer = new ArrayBuffer(4 + pathBytes.length);
const view = new DataView(buffer);
view.setUint8(0, FRAME.REQUEST);
view.setUint16(1, 1 + pathBytes.length, true);
view.setUint8(3, DATA.TAGS_GET);
const uint8Buffer = new Uint8Array(buffer);
uint8Buffer.set(pathBytes, 4);
return buffer;
}