Skip to content

Commit 0cd40a9

Browse files
committed
Bug fixing / UI improvements
1 parent 76e696f commit 0cd40a9

8 files changed

Lines changed: 985 additions & 15 deletions

File tree

frontend/src/bleclient.js

Lines changed: 279 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ const ProtocolMessageType = {
1515
REQUEST : 0x01,
1616
RESPONSE : 0x02,
1717
EVENT : 0x03,
18-
CONTROL : 0x04
18+
CONTROL : 0x04,
19+
STREAM_DATA : 0x05,
20+
STREAM_START : 0x06,
21+
STREAM_END : 0x07
1922
};
2023

2124
// Control Message Types (INTERNAL - for control channel)
@@ -26,7 +29,9 @@ const ControlMessageType = {
2629
BUFFER_FULL : 0x04,
2730
RESET : 0x05,
2831
MTU_INFO : 0x06,
29-
REQUEST_MTU_INFO : 0x07
32+
REQUEST_MTU_INFO : 0x07,
33+
STREAM_ACK : 0x07,
34+
STREAM_ERROR : 0x09
3035
};
3136

3237
// Fragment Flags (INTERNAL)
@@ -69,6 +74,9 @@ export class BLEClient {
6974

7075
// Disconnect listener
7176
this.onDisconnectCallback = null;
77+
78+
// Streaming file transfer state
79+
this.activeStreams = new Map();
7280
}
7381

7482
// Logging helpers
@@ -458,11 +466,93 @@ export class BLEClient {
458466
this.fragmentBuffers.clear();
459467
break;
460468

469+
case ControlMessageType.STREAM_ACK:
470+
if (dataView.byteLength >= 4) {
471+
const streamId = dataView.getUint8(1);
472+
const chunkIndex = dataView.getUint16(2, true); // little-endian
473+
this.handleStreamAck(streamId, chunkIndex);
474+
}
475+
break;
476+
477+
case ControlMessageType.STREAM_ERROR:
478+
if (dataView.byteLength >= 3) {
479+
const streamId = dataView.getUint8(1);
480+
const errorCode = dataView.getUint8(2);
481+
this.handleStreamError(streamId, errorCode);
482+
}
483+
break;
484+
461485
default:
462486
this.logWarn(`Unknown control message type: ${ctrlType}`);
463487
}
464488
}
465489

490+
/**
491+
* Handle stream ACK (INTERNAL)
492+
* @param {number} streamId
493+
* @param {number} chunkIndex - 0xFFFF = start ACK, 0xFFFE = completion ACK
494+
*/
495+
handleStreamAck(streamId, chunkIndex) {
496+
const stream = this.activeStreams.get(streamId);
497+
if (!stream) {
498+
this.logWarn(`STREAM_ACK for unknown stream ${streamId}`);
499+
return;
500+
}
501+
502+
if (chunkIndex === 0xFFFF) {
503+
this.logVerbose(`Stream ${streamId} start acknowledged`);
504+
stream.startAcked = true;
505+
if (stream.startResolve) {
506+
stream.startResolve();
507+
}
508+
} else if (chunkIndex === 0xFFFE) {
509+
this.logInfo(`Stream ${streamId} complete`);
510+
stream.complete = true;
511+
if (stream.completeResolve) {
512+
stream.completeResolve();
513+
}
514+
} else {
515+
stream.ackedChunks.add(chunkIndex);
516+
stream.lastAckedChunk = chunkIndex;
517+
if (stream.onProgress) {
518+
const bytesAcked = Math.min((chunkIndex + 1) * stream.chunkPayloadSize, stream.totalSize);
519+
stream.onProgress(bytesAcked, stream.totalSize);
520+
}
521+
}
522+
}
523+
524+
/**
525+
* Handle stream error (INTERNAL)
526+
* @param {number} streamId
527+
* @param {number} errorCode
528+
*/
529+
handleStreamError(streamId, errorCode) {
530+
const stream = this.activeStreams.get(streamId);
531+
if (!stream) {
532+
this.logWarn(`STREAM_ERROR for unknown stream ${streamId}`);
533+
return;
534+
}
535+
536+
const errorMessages = {
537+
0x01: 'Invalid header',
538+
0x02: 'Invalid metadata',
539+
0x03: 'Mutex error',
540+
0x04: 'Too many concurrent streams',
541+
0x05: 'Cannot create file',
542+
0x06: 'Unknown stream',
543+
0x07: 'SD write failed',
544+
0x08: 'Timeout'
545+
};
546+
547+
const message = errorMessages[errorCode] || `Unknown error (0x${errorCode.toString(16)})`;
548+
this.logError(`Stream ${streamId} error: ${message}`);
549+
550+
stream.error = new Error(`Stream error: ${message}`);
551+
if (stream.errorReject) {
552+
stream.errorReject(stream.error);
553+
}
554+
}
555+
466556
/**
467557
* Send fragmented message (INTERNAL)
468558
* @param {BLECharacteristic} characteristic
@@ -632,6 +722,193 @@ export class BLEClient {
632722
await this.controlChar.writeValue(data);
633723
}
634724

725+
/**
726+
* Upload file using streaming binary protocol (memory-efficient)
727+
* @param {string} projectId - Project identifier
728+
* @param {string} filename - Target filename
729+
* @param {ArrayBuffer|Uint8Array|string} content - File content
730+
* @param {function} onProgress - Progress callback (bytesUploaded, totalBytes)
731+
* @returns {Promise<boolean>} - Success status
732+
*/
733+
async uploadFileStreaming(projectId, filename, content, onProgress = null) {
734+
if (!this.connected) {
735+
throw new Error('Not connected');
736+
}
737+
738+
// Convert content to Uint8Array
739+
let data;
740+
if (typeof content === 'string') {
741+
data = new TextEncoder().encode(content);
742+
} else if (content instanceof ArrayBuffer) {
743+
data = new Uint8Array(content);
744+
} else {
745+
data = content;
746+
}
747+
748+
const streamId = this.currentMessageId++ & 0xFF;
749+
const totalSize = data.length;
750+
const chunkPayloadSize = this.mtu - 5; // 5-byte header for STREAM_DATA
751+
const chunkCount = Math.ceil(totalSize / chunkPayloadSize) || 1;
752+
753+
this.logInfo(`Starting streaming upload: stream=${streamId}, file=${filename}, size=${totalSize}, chunks=${chunkCount}`);
754+
755+
// Initialize stream state
756+
const streamState = {
757+
streamId: streamId,
758+
totalSize: totalSize,
759+
chunkPayloadSize: chunkPayloadSize,
760+
ackedChunks: new Set(),
761+
lastAckedChunk: -1,
762+
startAcked: false,
763+
complete: false,
764+
error: null,
765+
onProgress: onProgress,
766+
startResolve: null,
767+
completeResolve: null,
768+
errorReject: null
769+
};
770+
this.activeStreams.set(streamId, streamState);
771+
772+
try {
773+
// 1. Send STREAM_START
774+
const metadata = JSON.stringify({ project: projectId, filename: filename });
775+
const metadataBytes = new TextEncoder().encode(metadata);
776+
const startPacket = new Uint8Array(9 + metadataBytes.length);
777+
startPacket[0] = ProtocolMessageType.STREAM_START;
778+
startPacket[1] = streamId;
779+
// Total size (little-endian uint32)
780+
startPacket[2] = totalSize & 0xFF;
781+
startPacket[3] = (totalSize >> 8) & 0xFF;
782+
startPacket[4] = (totalSize >> 16) & 0xFF;
783+
startPacket[5] = (totalSize >> 24) & 0xFF;
784+
// Chunk count (little-endian uint16)
785+
startPacket[6] = chunkCount & 0xFF;
786+
startPacket[7] = (chunkCount >> 8) & 0xFF;
787+
startPacket[8] = 0x01; // Flags: overwrite
788+
startPacket.set(metadataBytes, 9);
789+
790+
await this.requestChar.writeValueWithResponse(startPacket);
791+
this.logVerbose(`Stream ${streamId}: STREAM_START sent`);
792+
793+
// Wait for start ACK
794+
await this.waitForStreamEvent(streamState, 'start', 5000);
795+
this.logVerbose(`Stream ${streamId}: Start acknowledged`);
796+
797+
// 2. Send STREAM_DATA chunks
798+
for (let i = 0; i < chunkCount; i++) {
799+
if (streamState.error) {
800+
throw streamState.error;
801+
}
802+
803+
const start = i * chunkPayloadSize;
804+
const end = Math.min(start + chunkPayloadSize, totalSize);
805+
const chunkData = data.slice(start, end);
806+
const isLast = (i === chunkCount - 1);
807+
808+
const dataPacket = new Uint8Array(5 + chunkData.length);
809+
dataPacket[0] = ProtocolMessageType.STREAM_DATA;
810+
dataPacket[1] = streamId;
811+
dataPacket[2] = i & 0xFF;
812+
dataPacket[3] = (i >> 8) & 0xFF;
813+
dataPacket[4] = isLast ? 0x01 : 0x00;
814+
dataPacket.set(chunkData, 5);
815+
816+
await this.requestChar.writeValueWithResponse(dataPacket);
817+
818+
// Flow control: wait every 10 chunks if we're getting too far ahead
819+
if (i % 10 === 9 && i < chunkCount - 1) {
820+
const targetAck = Math.max(0, i - 5);
821+
try {
822+
await this.waitForCondition(
823+
() => streamState.lastAckedChunk >= targetAck || streamState.error,
824+
2000
825+
);
826+
} catch (e) {
827+
// Timeout on flow control is a warning, not an error
828+
this.logWarn(`Stream ${streamId}: Flow control timeout, continuing...`);
829+
}
830+
}
831+
832+
if (streamState.error) {
833+
throw streamState.error;
834+
}
835+
}
836+
837+
this.logVerbose(`Stream ${streamId}: All chunks sent`);
838+
839+
// 3. Send STREAM_END
840+
const endPacket = new Uint8Array(6);
841+
endPacket[0] = ProtocolMessageType.STREAM_END;
842+
endPacket[1] = streamId;
843+
endPacket[2] = totalSize & 0xFF;
844+
endPacket[3] = (totalSize >> 8) & 0xFF;
845+
endPacket[4] = (totalSize >> 16) & 0xFF;
846+
endPacket[5] = (totalSize >> 24) & 0xFF;
847+
848+
await this.requestChar.writeValueWithResponse(endPacket);
849+
this.logVerbose(`Stream ${streamId}: STREAM_END sent`);
850+
851+
// Wait for completion ACK
852+
await this.waitForStreamEvent(streamState, 'complete', 5000);
853+
854+
this.logInfo(`Stream ${streamId} completed successfully`);
855+
return true;
856+
857+
} catch (error) {
858+
this.logError(`Stream ${streamId} failed:`, error);
859+
throw error;
860+
} finally {
861+
this.activeStreams.delete(streamId);
862+
}
863+
}
864+
865+
/**
866+
* Wait for a stream event (start ACK, completion, or error)
867+
* @param {object} streamState
868+
* @param {string} eventType - 'start' or 'complete'
869+
* @param {number} timeoutMs
870+
*/
871+
async waitForStreamEvent(streamState, eventType, timeoutMs) {
872+
return new Promise((resolve, reject) => {
873+
// Set up error rejection
874+
streamState.errorReject = reject;
875+
876+
if (eventType === 'start') {
877+
if (streamState.startAcked) {
878+
resolve();
879+
return;
880+
}
881+
streamState.startResolve = resolve;
882+
} else if (eventType === 'complete') {
883+
if (streamState.complete) {
884+
resolve();
885+
return;
886+
}
887+
streamState.completeResolve = resolve;
888+
}
889+
890+
// Timeout
891+
setTimeout(() => {
892+
reject(new Error(`Stream ${eventType} timeout (${timeoutMs}ms)`));
893+
}, timeoutMs);
894+
});
895+
}
896+
897+
/**
898+
* Wait for a condition to become true
899+
* @param {function} condition - Function that returns boolean
900+
* @param {number} timeoutMs
901+
*/
902+
async waitForCondition(condition, timeoutMs) {
903+
const startTime = Date.now();
904+
while (!condition()) {
905+
if (Date.now() - startTime > timeoutMs) {
906+
throw new Error('Condition timeout');
907+
}
908+
await this.sleep(50);
909+
}
910+
}
911+
635912
/**
636913
* Helper function: Sleep
637914
* @param {number} ms

0 commit comments

Comments
 (0)