From 5c3389196885fdd9c9f40942911946b7baf24581 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Tue, 10 Feb 2026 13:53:46 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20size=20limits=20and=20stora?= =?UTF-8?q?ge=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/core/beambox-uploader.ts | 159 +++++++++++++++++++----- src/lib/protocol/interfaces/config.ts | 12 ++ src/lib/protocol/interfaces/defaults.ts | 4 + 3 files changed, 147 insertions(+), 28 deletions(-) diff --git a/src/lib/core/beambox-uploader.ts b/src/lib/core/beambox-uploader.ts index f5cd86d..95ef6ae 100644 --- a/src/lib/core/beambox-uploader.ts +++ b/src/lib/core/beambox-uploader.ts @@ -33,6 +33,7 @@ export class BeamBoxUploader { private imageProcessor: ImageProcessor; private payloadBuilder: PayloadBuilder; private imageConfig: ImageConfig; + private protocolConfig: ProtocolConfig; private verbose: boolean = false; constructor( @@ -45,17 +46,16 @@ export class BeamBoxUploader { ) { this.verbose = verbose; this.imageConfig = imageConfig ?? DEFAULT_IMAGE_CONFIG; + this.protocolConfig = protocolConfig ?? DEFAULT_PROTOCOL_CONFIG; this.ble = new BleUploader( deviceAddress ?? null, chunkDelay ?? null, bleConfig ?? DEFAULT_BLE_CONFIG, - protocolConfig ?? DEFAULT_PROTOCOL_CONFIG, + this.protocolConfig, verbose, ); this.imageProcessor = new ImageProcessor(this.imageConfig); - this.payloadBuilder = new PayloadBuilder( - protocolConfig ?? DEFAULT_PROTOCOL_CONFIG, - ); + this.payloadBuilder = new PayloadBuilder(this.protocolConfig); } /** @@ -135,15 +135,7 @@ export class BeamBoxUploader { logger.info("Waiting for device to be fully ready..."); await this.sleep(1000); - // Step 1: Send image info packet to announce upload - const imageInfoPayload = this.payloadBuilder.buildImageInfo( - PacketType.IMAGE, - 1, - ); - await this.ble.sendImageInfo(imageInfoPayload); - logger.info("Sent image info packet, proceeding to data transfer"); - - // Step 2: Build and send image data payload + // Build image data payload const fullData = this.payloadBuilder.buildImageData( jpegData, effectiveSize, @@ -154,6 +146,26 @@ export class BeamBoxUploader { `Payload bytes: total=${fullData.length}, jpeg=${jpegData.length}, header+prefix=${prefixLen}`, ); + // Validate against protocol limits + this.validatePayloadLimits(fullData.length); + + // Check device storage before upload + if (!this.checkStorageCapacity(fullData.length)) { + throw new UploadError( + `Insufficient device storage. Image requires ${Math.ceil(fullData.length / 1024)}KB. ` + + `Try reducing image size or quality.`, + ); + } + + // Step 1: Send image info packet to announce upload + const imageInfoPayload = this.payloadBuilder.buildImageInfo( + PacketType.IMAGE, + 1, + ); + await this.ble.sendImageInfo(imageInfoPayload); + logger.info("Sent image info packet, proceeding to data transfer"); + + // Step 2: Send image data payload // Send data in chunks with protocol packets const ok = await this.ble.sendData(fullData, PacketType.IMAGE, onProgress); @@ -171,6 +183,74 @@ export class BeamBoxUploader { return true; } + /** + * Check if device has enough free storage for the upload + * @param payloadSizeBytes Size of the payload in bytes + * @returns True if enough space available, false otherwise + */ + private checkStorageCapacity(payloadSizeBytes: number): boolean { + const deviceStatus = this.ble.getDeviceStatus(); + if (!deviceStatus) { + logger.warning("Device status not available, cannot check storage"); + return false; + } + + const freespaceKB = Number(deviceStatus.freespace) || 0; + const freespaceBytes = freespaceKB * 1024; + const payloadKB = Math.ceil(payloadSizeBytes / 1024); + + logger.info( + `Storage check: payload=${payloadKB}KB, available=${freespaceKB}KB`, + ); + + // Add 10% safety margin to avoid filling device completely + const requiredBytes = Math.ceil(payloadSizeBytes * 1.1); + + if (freespaceBytes < requiredBytes) { + logger.error( + `Insufficient storage: need ${Math.ceil(requiredBytes / 1024)}KB (with 10% margin), have ${freespaceKB}KB`, + ); + return false; + } + + return true; + } + + /** + * Validate payload against protocol limits + * @param payloadSizeBytes Size of the payload in bytes + * @throws UploadError if payload exceeds safety limits + */ + private validatePayloadLimits(payloadSizeBytes: number): void { + // Check 2MB payload size limit (from iOS app) + if (payloadSizeBytes > this.protocolConfig.maxPayloadSize) { + const sizeMB = (payloadSizeBytes / (1024 * 1024)).toFixed(2); + const limitMB = ( + this.protocolConfig.maxPayloadSize / + (1024 * 1024) + ).toFixed(2); + throw new UploadError( + `Payload too large: ${sizeMB}MB exceeds ${limitMB}MB limit. ` + + `Reduce image quality, frame count, or dimensions.`, + ); + } + + // Check packet count limit (from iOS app: 20000 packets max) + const packetCount = Math.ceil( + payloadSizeBytes / this.protocolConfig.chunkSize, + ); + if (packetCount > this.protocolConfig.maxPacketCount) { + throw new UploadError( + `Too many packets: ${packetCount} exceeds ${this.protocolConfig.maxPacketCount} limit. ` + + `Reduce payload size or increase chunk size.`, + ); + } + + logger.info( + `Payload validation passed: ${(payloadSizeBytes / 1024).toFixed(2)}KB, ${packetCount} packets`, + ); + } + /** * Upload an animated GIF or video as Type 5 (DYNAMIC_AMBIENCE) * @@ -185,7 +265,7 @@ export class BeamBoxUploader { onProgress?: (progress: number) => void, ): Promise { const animationSize: [number, number] = targetSize; - + // Extract frames from the file logger.info( `Extracting frames from animation at ${animationSize[0]}x${animationSize[1]}...`, @@ -200,11 +280,13 @@ export class BeamBoxUploader { // The xV4 format requires at least 2 frames const MIN_FRAMES = 2; if (frames.length < MIN_FRAMES) { - logger.info(`Padding from ${frames.length} to ${MIN_FRAMES} frames by duplicating last frame`); + logger.info( + `Padding from ${frames.length} to ${MIN_FRAMES} frames by duplicating last frame`, + ); while (frames.length < MIN_FRAMES) { const lastFrame = frames[frames.length - 1]!; frames.push({ - name: `frame_${String(frames.length + 1).padStart(5, '0')}`, + name: `frame_${String(frames.length + 1).padStart(5, "0")}`, data: lastFrame.data, // Reuse the same buffer }); } @@ -223,15 +305,7 @@ export class BeamBoxUploader { logger.info("Waiting for device to be fully ready..."); await this.sleep(1000); - // Step 1: Send image info packet (Type 6 for the info) - const imageInfoPayload = this.payloadBuilder.buildImageInfo( - PacketType.IMAGE, - 1, - ); - await this.ble.sendImageInfo(imageInfoPayload); - logger.info("Sent animation info packet, proceeding to data transfer"); - - // Step 2: Build and send animation data payload (Type 5) + // Build animation payload to check size const fullData = this.payloadBuilder.buildAnimationData( frames, intervalMs, @@ -242,6 +316,26 @@ export class BeamBoxUploader { `Animation payload bytes: total=${fullData.length}, frames=${frames.length}`, ); + // Validate against protocol limits + this.validatePayloadLimits(fullData.length); + + // Check device storage before upload + if (!this.checkStorageCapacity(fullData.length)) { + throw new UploadError( + `Insufficient device storage. Animation requires ${Math.ceil(fullData.length / 1024)}KB. ` + + `Try reducing frames or image quality.`, + ); + } + + // Step 1: Send image info packet (Type 6 for the info) + const imageInfoPayload = this.payloadBuilder.buildImageInfo( + PacketType.IMAGE, + 1, + ); + await this.ble.sendImageInfo(imageInfoPayload); + logger.info("Sent animation info packet, proceeding to data transfer"); + + // Step 2: Send animation data payload (Type 5) // Send data in chunks with protocol packets using DYNAMIC_AMBIENCE packet type const ok = await this.ble.sendData( fullData, @@ -277,7 +371,12 @@ export class BeamBoxUploader { animationSize?: [number, number], onProgress?: (progress: number, status?: string) => void, ): Promise { - return await this.upload({ imagePath, targetSize, animationSize, onProgress }); + return await this.upload({ + imagePath, + targetSize, + animationSize, + onProgress, + }); } /** @@ -354,7 +453,9 @@ export class BeamBoxUploader { // Log progress if we got new notifications if (notifications.length > lastNotificationCount) { - logger.debug(`Received ${notifications.length} total notifications so far...`); + logger.debug( + `Received ${notifications.length} total notifications so far...`, + ); lastNotificationCount = notifications.length; } @@ -380,7 +481,9 @@ export class BeamBoxUploader { if (notifications.length > 0 && this.verbose) { logger.debug("Received notification types:"); notifications.forEach((n, i) => { - logger.debug(` [${i}] type: ${n.parsed?.jsonData?.type || 'unknown'}, data: ${JSON.stringify(n.parsed?.jsonData || {})}`); + logger.debug( + ` [${i}] type: ${n.parsed?.jsonData?.type || "unknown"}, data: ${JSON.stringify(n.parsed?.jsonData || {})}`, + ); }); } } diff --git a/src/lib/protocol/interfaces/config.ts b/src/lib/protocol/interfaces/config.ts index 7db2b82..f364e7e 100644 --- a/src/lib/protocol/interfaces/config.ts +++ b/src/lib/protocol/interfaces/config.ts @@ -70,6 +70,18 @@ export interface ProtocolConfig { * Maximum time (in seconds) to wait for a packet acknowledgment from the device */ packetAckTimeout: number; + + /** + * Maximum number of packets allowed per upload + * Value: 20000 packets max + */ + maxPacketCount: number; + + /** + * Maximum payload size in bytes allowed per upload + * Value: 2MB (2 * 1024 * 1024 bytes) + */ + maxPayloadSize: number; } /** diff --git a/src/lib/protocol/interfaces/defaults.ts b/src/lib/protocol/interfaces/defaults.ts index 96197f0..06b8aa9 100644 --- a/src/lib/protocol/interfaces/defaults.ts +++ b/src/lib/protocol/interfaces/defaults.ts @@ -26,6 +26,8 @@ export const DEFAULT_BLE_CONFIG: BLEConfig = { * - packetDelay: 0.1s - prevents overwhelming device buffer * - imageInfoDelay: 0.01s - gives device time to prepare * - packetAckTimeout: 2.0s - reasonable wait for response + * - maxPacketCount: 20000 - seem to be the app limit + * - maxPayloadSize: 2MB - seem to be the app limit */ export const DEFAULT_PROTOCOL_CONFIG: ProtocolConfig = { cmdType: CMD_TYPE, @@ -35,6 +37,8 @@ export const DEFAULT_PROTOCOL_CONFIG: ProtocolConfig = { packetDelay: 0.1, imageInfoDelay: 0.01, packetAckTimeout: 2.0, + maxPacketCount: 20000, + maxPayloadSize: 2 * 1024 * 1024, }; /**