mirror of
https://github.com/YuzuZensai/beamboxctl.git
synced 2026-07-21 20:42:19 +00:00
✨ feat: size limits and storage checks
This commit is contained in:
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
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 || {})}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user