import { FRAME, DATA, ZEPHYR_ERRORS } from './constants'; import { protocolInfo, deviceInfo, fsInfo, transferStats, fwInfo, resetTransferStats, transferDetails } from '../store'; import { addToast } from '../toast'; import { SETTINGS } from '../settings'; 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; export type FrameSender = (buffer: ArrayBuffer) => Promise; let lsBuffer: any[] = []; let fileChunks: Uint8Array[] = []; let lsTimeout: ReturnType | null = null; let lsResolve: ((data: any[]) => void) | null = null; let lsReject: ((error: Error) => 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) { const errorInfo = ZEPHYR_ERRORS[errorCode]; const hexCode = errorCode.toString(16).padStart(2, '0'); if (errorInfo) { // Variante mit HTML-Tags (erfordert {@html ...} im Svelte-Template) const htmlMessage = `Buzzer: ${errorInfo.zephyr} (0x${hexCode}): ${errorInfo.text} `; addToast(htmlMessage, 'error'); /* Alternativ als reiner Text, falls HTML im Toast nicht unterstützt wird: const textMessage = `Buzzer: [${errorInfo.zephyr}] ${errorInfo.text} (0x${hexCode})`; addToast(textMessage, 'error'); */ } else { addToast(`Der Buzzer meldet einen unbekannten Fehler: 0x${hexCode}`, 'error'); } } export function parseIncomingFrame(view: DataView, sender: FrameSender) { if (view.byteLength < 3) return; const frameType = view.getUint8(0); const payloadLength = view.getUint16(1, true); switch (frameType) { case FRAME.RESPONSE: const dataType = view.getUint8(3); switch (dataType) { case DATA.PROTO_INFO: const version = view.getUint16(4, true); const maxChunkSize = view.getUint16(6, true); protocolInfo.set({ version, maxChunkSize }); break; case DATA.FS_INFO: const totalSizeBytes = view.getUint32(4, true); const freeSizeBytes = view.getUint32(8, true); const maxPathLength = view.getUint8(12); const sysPathLength = view.getUint8(13); const audioPathLength = view.getUint8(14); const sysPath = new TextDecoder().decode(new Uint8Array(view.buffer, 15, sysPathLength)); const audioPath = new TextDecoder().decode(new Uint8Array(view.buffer, 15 + sysPathLength, audioPathLength)); fsInfo.set({ totalSize: totalSizeBytes / 1024 / 1024, freeSize: freeSizeBytes / 1024 / 1024, maxPathLength, sysPath, audioPath }); break; case DATA.DEVICE_INFO: const deviceId = [0, 2, 4, 6] .map(offset => view.getUint16(4 + offset, false).toString(16).padStart(4, '0').toUpperCase()) .join('-'); const boardNameLength = view.getUint8(12); const boardRevisionLength = view.getUint8(13); const socNameLength = view.getUint8(14); const boardName = new TextDecoder().decode(new Uint8Array(view.buffer, 15, boardNameLength)); const boardRevision = new TextDecoder().decode(new Uint8Array(view.buffer, 15 + boardNameLength, boardRevisionLength)); const socName = new TextDecoder().decode(new Uint8Array(view.buffer, 15 + boardNameLength + boardRevisionLength, socNameLength)); deviceInfo.set({ deviceId, boardName, boardRevision, socName }); break; case DATA.FW_INFO: const fwStatus = view.getUint8(4); const slot1Size = view.getUint32(5, true); const fw_version_length = view.getUint8(9); const kernel_version_length = view.getUint8(10); const fwVersion = new TextDecoder().decode(new Uint8Array(view.buffer, 11, fw_version_length)); const kernelVersion = new TextDecoder().decode(new Uint8Array(view.buffer, 11 + fw_version_length, kernel_version_length)); fwInfo.set({ fwStatus, slot1Size, fwVersion, kernelVersion }); } break; case FRAME.LS_START: lsBuffer = []; resetLsWatchdog(); handleLsStart(sender); break; case FRAME.LS_ENTRY: resetLsWatchdog(); if (payloadLength >= 6) { const type = view.getUint8(3); const size = view.getUint32(4, true); const nameLen = view.getUint8(8); const name = new TextDecoder().decode(new Uint8Array(view.buffer, 9, nameLen)); lsBuffer.push({ type, size, name }); // TODO: Mehr credits senden, wenn diese knapp werden } break; case FRAME.LS_END: if (lsTimeout) clearTimeout(lsTimeout); const total = view.getUint32(3, true); 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'); } else if (lsResolve) { const currentResolve = lsResolve; lsResolve = null; lsReject = null; currentResolve([...lsBuffer]); } break; case FRAME.FILE_START: currentFileCrc32 = 0; const totalBytes = view.getUint32(3, true); const nowStart = performance.now(); fileChunks = []; 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; fileTransfer.receivedBytes = 0; fileTransfer.active = true; fileTransfer.startTime = nowStart; lastUiUpdate = 0; fileTransfer.metricsTimer = setInterval(() => { if (!fileTransfer.active) return; // Watchdog-Logik: Prüfen ob seit der letzten Sekunde Daten kamen if (fileTransfer.receivedBytes === fileTransfer.lastReceivedBytes) { fileTransfer.stalledSeconds++; if (fileTransfer.stalledSeconds >= 5) { // 5 Sekunden Timeout console.warn("[FILE_GET] Übertragung abgebrochen: Timeout (Keine Daten empfangen)."); if (fileTransfer.metricsTimer) clearInterval(fileTransfer.metricsTimer); fileTransfer.active = false; addToast("Dateitransfer abgebrochen (Timeout)", "error"); const currentReject = fileGetReject; fileGetResolve = null; fileGetReject = null; if (currentReject) { currentReject(new Error("Timeout beim Dateitransfer")); } return; } } else { fileTransfer.stalledSeconds = 0; fileTransfer.lastReceivedBytes = fileTransfer.receivedBytes; } }, 1000); // Initiale Credits (z.B. 64) fileTransfer.credits = 128; sendCredits(fileTransfer.credits, sender); break; case FRAME.FILE_CHUNK: if (!fileTransfer.active) break; const chunkData = new Uint8Array(view.buffer, 3, payloadLength); currentFileCrc32 = crc32(chunkData, currentFileCrc32); fileChunks.push(new Uint8Array(chunkData)); const previousReceived = fileTransfer.receivedBytes; fileTransfer.receivedBytes += payloadLength; fileTransfer.credits--; 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) })); lastUiUpdate = nowChunk; } } if (fileTransfer.credits <= 64) { fileTransfer.credits = 128; sendCredits(fileTransfer.credits, sender); } break; case FRAME.FILE_END: if (fileTransfer.mode === 'file') { transferStats.update(s => ({ ...s, bytesDone: s.bytesTotal, })); } if (fileTransfer.metricsTimer) clearInterval(fileTransfer.metricsTimer); fileTransfer.active = false; const buzzerCrc32 = view.getUint32(3, true); const totalElapsed = (performance.now() - fileTransfer.startTime) / 1000; const avgSpeed = (fileTransfer.receivedBytes / 1024) / totalElapsed; if (currentFileCrc32 === buzzerCrc32) { const fileBlob = new Blob(fileChunks, { type: 'application/octet-stream' }); if (fileTransfer.mode === 'file') { const fileName = get(transferStats).currentFileName; const currentResolve = fileGetResolve; const currentReject = fileGetReject; // Direkt hier aufräumen, um Race Conditions bei schnellen Folge-Transfers zu vermeiden fileGetResolve = null; fileGetReject = null; saveLocalFile(fileName, fileBlob, fileTransfer.totalBytes) .then(() => { refreshLocal(); if (currentResolve) { currentResolve({ success: true }); } }) .catch(err => { console.error("Datenbankfehler:", err); addToast(`Speichern von ${fileName} fehlgeschlagen.`, 'error'); if (currentReject) currentReject(err); }); } else { // TAGS Modus: Blob direkt zurückgeben, nichts speichern const currentResolve = fileGetResolve; fileGetResolve = null; fileGetReject = null; if (currentResolve) currentResolve({ success: true, blob: fileBlob }); } } else { console.error("[CRC] Mismatch! Datei beschädigt."); addToast("CRC Fehler: Datei wurde fehlerhaft übertragen.", "error"); const currentReject = fileGetReject; fileGetResolve = null; fileGetReject = null; if (currentReject) currentReject(new Error("CRC Mismatch")); } 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); const errorInfo = ZEPHYR_ERRORS[errorCode]; if (errorInfo) { console.error( `Received error frame: 0x${errorCode.toString(16).padStart(2, '0')} (${errorInfo.zephyr}) - ${errorInfo.text}` ); } else { console.error(`Received error frame with code: 0x${errorCode.toString(16).padStart(2, '0')}`); } showErrorToast(errorCode); if (lsReject) { const currentReject = lsReject; lsResolve = null; lsReject = null; currentReject(new Error(`Buzzer Error 0x${errorCode.toString(16)}`)); } if (fileGetReject && fileTransfer.active) { if (fileTransfer.metricsTimer) clearInterval(fileTransfer.metricsTimer); fileTransfer.active = false; const currentReject = fileGetReject; fileGetResolve = null; fileGetReject = null; currentReject(new Error(`Buzzer Error 0x${errorCode.toString(16)}`)); } if (uploadState.active && uploadState.onError) { uploadState.onError(new Error(`Buzzer Error 0x${errorCode.toString(16)}`)); } break; default: console.error(`Unknown frame type received: 0x${frameType.toString(16)}`); addToast(`Unbekannter Frame-Typ empfangen: 0x${frameType.toString(16)}`, 'error'); } } export function buildProtocolInfoRequest(): ArrayBuffer { const buffer = new ArrayBuffer(4); const view = new DataView(buffer); view.setUint8(0, FRAME.REQUEST); view.setUint16(1, 1, true); view.setUint8(3, DATA.PROTO_INFO); return buffer; } export function buildDeviceInfoRequest(): ArrayBuffer { const buffer = new ArrayBuffer(4); const view = new DataView(buffer); view.setUint8(0, FRAME.REQUEST); view.setUint16(1, 1, true); view.setUint8(3, DATA.DEVICE_INFO); return buffer; } export function buildFSInfoRequest(): ArrayBuffer { const buffer = new ArrayBuffer(4); const view = new DataView(buffer); view.setUint8(0, FRAME.REQUEST); view.setUint16(1, 1, true); view.setUint8(3, DATA.FS_INFO); return buffer; } export function buildFWInfoRequest(): ArrayBuffer { const buffer = new ArrayBuffer(4); const view = new DataView(buffer); view.setUint8(0, FRAME.REQUEST); view.setUint16(1, 1, true); view.setUint8(3, DATA.FW_INFO); return buffer; } export function buildLSRequest(path: string): ArrayBuffer { const encoder = new TextEncoder(); const pathBytes = encoder.encode(path); console.debug(`[Protocol] LS request for path: ${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); // Payload: DataType(1) + String view.setUint8(3, DATA.LS); const uint8Buffer = new Uint8Array(buffer); uint8Buffer.set(pathBytes, 4); return buffer; } async function handleLsStart(send: FrameSender) { lsBuffer = []; await sendCredits(64, send); } async function sendCredits(count: number, send: FrameSender) { const buffer = new ArrayBuffer(5); const view = new DataView(buffer); view.setUint8(0, FRAME.ACK); view.setUint16(1, 2, true); view.setUint16(3, count, true); await send(buffer); } function resetLsWatchdog() { if (lsTimeout) clearTimeout(lsTimeout); lsTimeout = setTimeout(() => { addToast("Verzeichnis-Streaming abgebrochen (Timeout)", "warning"); lsBuffer = []; if (lsReject) { const currentReject = lsReject; lsResolve = null; lsReject = null; currentReject(new Error("Timeout beim Lesen des Verzeichnisses")); } }, 3000); } export function setLsResolver(resolve: (data: any[]) => void, reject: (error: Error) => void) { lsResolve = resolve; lsReject = reject; } const fileTransfer = { active: false, mode: 'file' as 'file' | 'tags', startTime: 0, totalBytes: 0, receivedBytes: 0, lastReceivedBytes: 0, stalledSeconds: 0, credits: 0, metricsTimer: null as ReturnType | null }; 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 { const encoder = new TextEncoder(); const pathBytes = encoder.encode(path); console.debug(`[Protocol] FILE_GET request for path: ${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.FILE_GET); 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); console.debug(`[Protocol] TAGS_GET request for path: ${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; }