vor ble umbau
This commit is contained in:
309
webpage/src/lib/protocol/parser.ts
Normal file
309
webpage/src/lib/protocol/parser.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import { FRAME, DATA, ZEPHYR_ERRORS } from './constants';
|
||||
import { protocolInfo, fsInfo } from '../store';
|
||||
import { addToast } from '../toast';
|
||||
|
||||
export type FrameSender = (buffer: ArrayBuffer) => Promise<void>;
|
||||
|
||||
let lsBuffer: any[] = [];
|
||||
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 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: <span class="font-mono font-bold text-xs">${errorInfo.zephyr} (0x${hexCode}):</span> ${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: <span class="font-mono">0x${hexCode}</span>`, '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);
|
||||
|
||||
if (dataType === DATA.PROTO_INFO && payloadLength >= 5) {
|
||||
const version = view.getUint16(4, true);
|
||||
const maxChunkSize = view.getUint16(6, true);
|
||||
protocolInfo.set({ version, maxChunkSize });
|
||||
} else if (dataType === DATA.FS_INFO && payloadLength >= 14) {
|
||||
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 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);
|
||||
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');
|
||||
} else if (lsResolve) {
|
||||
lsResolve([...lsBuffer]);
|
||||
lsResolve = null;
|
||||
lsReject = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case FRAME.FILE_START:
|
||||
fileTransfer.totalBytes = view.getUint32(3, true);
|
||||
fileTransfer.receivedBytes = 0;
|
||||
fileTransfer.lastReceivedBytes = 0;
|
||||
fileTransfer.stalledSeconds = 0;
|
||||
fileTransfer.active = true;
|
||||
fileTransfer.startTime = performance.now();
|
||||
|
||||
console.log(`[FILE_GET] Stream gestartet. Erwartete Größe: ${fileTransfer.totalBytes} Bytes.`);
|
||||
|
||||
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;
|
||||
|
||||
// Hier optional einen Toast anzeigen lassen, falls importiert:
|
||||
// addToast("Dateitransfer abgebrochen (Timeout)", "error");
|
||||
|
||||
if (fileGetReject) {
|
||||
fileGetReject(new Error("Timeout beim Dateitransfer"));
|
||||
fileGetResolve = null;
|
||||
fileGetReject = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Daten fließen -> Watchdog zurücksetzen
|
||||
fileTransfer.stalledSeconds = 0;
|
||||
fileTransfer.lastReceivedBytes = fileTransfer.receivedBytes;
|
||||
}
|
||||
|
||||
const elapsedSec = (performance.now() - fileTransfer.startTime) / 1000;
|
||||
const speedKB = (fileTransfer.receivedBytes / 1024) / elapsedSec;
|
||||
const percent = fileTransfer.totalBytes > 0
|
||||
? ((fileTransfer.receivedBytes / fileTransfer.totalBytes) * 100).toFixed(1)
|
||||
: "0.0";
|
||||
|
||||
console.log(`[FILE_GET] Fortschritt: ${percent}% | Speed: ${speedKB.toFixed(2)} KB/s`);
|
||||
}, 1000);
|
||||
|
||||
// Initiale Credits (z.B. 64)
|
||||
fileTransfer.credits = 128;
|
||||
sendCredits(fileTransfer.credits, sender);
|
||||
break;
|
||||
|
||||
case FRAME.FILE_CHUNK:
|
||||
if (!fileTransfer.active) break;
|
||||
|
||||
fileTransfer.receivedBytes += payloadLength;
|
||||
fileTransfer.credits--;
|
||||
|
||||
// Nachladen, sobald die Credits auf 32 fallen (Dein Vorschlag)
|
||||
if (fileTransfer.credits <= 64) {
|
||||
fileTransfer.credits = 128;
|
||||
sendCredits(fileTransfer.credits, sender);
|
||||
|
||||
}
|
||||
break;
|
||||
|
||||
case FRAME.FILE_END:
|
||||
if (fileTransfer.metricsTimer) {
|
||||
clearInterval(fileTransfer.metricsTimer);
|
||||
fileTransfer.metricsTimer = null;
|
||||
}
|
||||
fileTransfer.active = false;
|
||||
|
||||
const crc32 = view.getUint32(3, true);
|
||||
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`);
|
||||
console.log(`[FILE_GET] Zephyr CRC32: 0x${crc32.toString(16).toUpperCase().padStart(8, '0')}`);
|
||||
|
||||
if (fileGetResolve) {
|
||||
fileGetResolve(true);
|
||||
fileGetResolve = null;
|
||||
fileGetReject = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case FRAME.ERROR:
|
||||
const errorCode = view.getUint16(3, true);
|
||||
console.error(`Received error frame with code: 0x${errorCode.toString(16)}`);
|
||||
showErrorToast(errorCode);
|
||||
if (lsReject) {
|
||||
lsReject(new Error(`Buzzer Error 0x${errorCode.toString(16)}`));
|
||||
lsResolve = null;
|
||||
lsReject = null;
|
||||
}
|
||||
if (fileGetReject && fileTransfer.active) {
|
||||
if (fileTransfer.metricsTimer) clearInterval(fileTransfer.metricsTimer);
|
||||
fileTransfer.active = false;
|
||||
fileGetReject(new Error(`Buzzer Error 0x${errorCode.toString(16)}`));
|
||||
fileGetResolve = null;
|
||||
fileGetReject = null;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Unknown frame type received: 0x${frameType.toString(16)}`);
|
||||
addToast(`Unbekannter Frame-Typ empfangen: <span class="font-mono">0x${frameType.toString(16)}</span>`, '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 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 buildLSRequest(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); // 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);
|
||||
|
||||
console.debug(`Sende ${count} Credits für Stream...`);
|
||||
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) {
|
||||
lsReject(new Error("Timeout beim Lesen des Verzeichnisses"));
|
||||
lsResolve = null;
|
||||
lsReject = null;
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
export function setLsResolver(resolve: (data: any[]) => void, reject: (error: Error) => void) {
|
||||
lsResolve = resolve;
|
||||
lsReject = reject;
|
||||
}
|
||||
|
||||
const fileTransfer = {
|
||||
active: false,
|
||||
startTime: 0,
|
||||
totalBytes: 0,
|
||||
receivedBytes: 0,
|
||||
lastReceivedBytes: 0, // NEU: Für die Timeout-Berechnung
|
||||
stalledSeconds: 0, // NEU: Zähler für Stillstand
|
||||
credits: 0,
|
||||
metricsTimer: null as ReturnType<typeof setInterval> | null
|
||||
};
|
||||
|
||||
export function setFileGetResolver(resolve: (success: boolean) => void, reject: (error: Error) => void) {
|
||||
fileGetResolve = resolve;
|
||||
fileGetReject = reject;
|
||||
}
|
||||
|
||||
export function buildFileGetRequest(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.FILE_GET);
|
||||
|
||||
const uint8Buffer = new Uint8Array(buffer);
|
||||
uint8Buffer.set(pathBytes, 4);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
Reference in New Issue
Block a user