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 imageProcessor: ImageProcessor;
|
||||||
private payloadBuilder: PayloadBuilder;
|
private payloadBuilder: PayloadBuilder;
|
||||||
private imageConfig: ImageConfig;
|
private imageConfig: ImageConfig;
|
||||||
|
private protocolConfig: ProtocolConfig;
|
||||||
private verbose: boolean = false;
|
private verbose: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -45,17 +46,16 @@ export class BeamBoxUploader {
|
|||||||
) {
|
) {
|
||||||
this.verbose = verbose;
|
this.verbose = verbose;
|
||||||
this.imageConfig = imageConfig ?? DEFAULT_IMAGE_CONFIG;
|
this.imageConfig = imageConfig ?? DEFAULT_IMAGE_CONFIG;
|
||||||
|
this.protocolConfig = protocolConfig ?? DEFAULT_PROTOCOL_CONFIG;
|
||||||
this.ble = new BleUploader(
|
this.ble = new BleUploader(
|
||||||
deviceAddress ?? null,
|
deviceAddress ?? null,
|
||||||
chunkDelay ?? null,
|
chunkDelay ?? null,
|
||||||
bleConfig ?? DEFAULT_BLE_CONFIG,
|
bleConfig ?? DEFAULT_BLE_CONFIG,
|
||||||
protocolConfig ?? DEFAULT_PROTOCOL_CONFIG,
|
this.protocolConfig,
|
||||||
verbose,
|
verbose,
|
||||||
);
|
);
|
||||||
this.imageProcessor = new ImageProcessor(this.imageConfig);
|
this.imageProcessor = new ImageProcessor(this.imageConfig);
|
||||||
this.payloadBuilder = new PayloadBuilder(
|
this.payloadBuilder = new PayloadBuilder(this.protocolConfig);
|
||||||
protocolConfig ?? DEFAULT_PROTOCOL_CONFIG,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,15 +135,7 @@ export class BeamBoxUploader {
|
|||||||
logger.info("Waiting for device to be fully ready...");
|
logger.info("Waiting for device to be fully ready...");
|
||||||
await this.sleep(1000);
|
await this.sleep(1000);
|
||||||
|
|
||||||
// Step 1: Send image info packet to announce upload
|
// Build image data payload
|
||||||
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
|
|
||||||
const fullData = this.payloadBuilder.buildImageData(
|
const fullData = this.payloadBuilder.buildImageData(
|
||||||
jpegData,
|
jpegData,
|
||||||
effectiveSize,
|
effectiveSize,
|
||||||
@@ -154,6 +146,26 @@ export class BeamBoxUploader {
|
|||||||
`Payload bytes: total=${fullData.length}, jpeg=${jpegData.length}, header+prefix=${prefixLen}`,
|
`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
|
// Send data in chunks with protocol packets
|
||||||
const ok = await this.ble.sendData(fullData, PacketType.IMAGE, onProgress);
|
const ok = await this.ble.sendData(fullData, PacketType.IMAGE, onProgress);
|
||||||
|
|
||||||
@@ -171,6 +183,74 @@ export class BeamBoxUploader {
|
|||||||
return true;
|
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)
|
* Upload an animated GIF or video as Type 5 (DYNAMIC_AMBIENCE)
|
||||||
*
|
*
|
||||||
@@ -185,7 +265,7 @@ export class BeamBoxUploader {
|
|||||||
onProgress?: (progress: number) => void,
|
onProgress?: (progress: number) => void,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const animationSize: [number, number] = targetSize;
|
const animationSize: [number, number] = targetSize;
|
||||||
|
|
||||||
// Extract frames from the file
|
// Extract frames from the file
|
||||||
logger.info(
|
logger.info(
|
||||||
`Extracting frames from animation at ${animationSize[0]}x${animationSize[1]}...`,
|
`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
|
// The xV4 format requires at least 2 frames
|
||||||
const MIN_FRAMES = 2;
|
const MIN_FRAMES = 2;
|
||||||
if (frames.length < MIN_FRAMES) {
|
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) {
|
while (frames.length < MIN_FRAMES) {
|
||||||
const lastFrame = frames[frames.length - 1]!;
|
const lastFrame = frames[frames.length - 1]!;
|
||||||
frames.push({
|
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
|
data: lastFrame.data, // Reuse the same buffer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -223,15 +305,7 @@ export class BeamBoxUploader {
|
|||||||
logger.info("Waiting for device to be fully ready...");
|
logger.info("Waiting for device to be fully ready...");
|
||||||
await this.sleep(1000);
|
await this.sleep(1000);
|
||||||
|
|
||||||
// Step 1: Send image info packet (Type 6 for the info)
|
// Build animation payload to check size
|
||||||
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)
|
|
||||||
const fullData = this.payloadBuilder.buildAnimationData(
|
const fullData = this.payloadBuilder.buildAnimationData(
|
||||||
frames,
|
frames,
|
||||||
intervalMs,
|
intervalMs,
|
||||||
@@ -242,6 +316,26 @@ export class BeamBoxUploader {
|
|||||||
`Animation payload bytes: total=${fullData.length}, frames=${frames.length}`,
|
`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
|
// Send data in chunks with protocol packets using DYNAMIC_AMBIENCE packet type
|
||||||
const ok = await this.ble.sendData(
|
const ok = await this.ble.sendData(
|
||||||
fullData,
|
fullData,
|
||||||
@@ -277,7 +371,12 @@ export class BeamBoxUploader {
|
|||||||
animationSize?: [number, number],
|
animationSize?: [number, number],
|
||||||
onProgress?: (progress: number, status?: string) => void,
|
onProgress?: (progress: number, status?: string) => void,
|
||||||
): Promise<boolean> {
|
): 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
|
// Log progress if we got new notifications
|
||||||
if (notifications.length > lastNotificationCount) {
|
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;
|
lastNotificationCount = notifications.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +481,9 @@ export class BeamBoxUploader {
|
|||||||
if (notifications.length > 0 && this.verbose) {
|
if (notifications.length > 0 && this.verbose) {
|
||||||
logger.debug("Received notification types:");
|
logger.debug("Received notification types:");
|
||||||
notifications.forEach((n, i) => {
|
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
|
* Maximum time (in seconds) to wait for a packet acknowledgment from the device
|
||||||
*/
|
*/
|
||||||
packetAckTimeout: number;
|
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
|
* - packetDelay: 0.1s - prevents overwhelming device buffer
|
||||||
* - imageInfoDelay: 0.01s - gives device time to prepare
|
* - imageInfoDelay: 0.01s - gives device time to prepare
|
||||||
* - packetAckTimeout: 2.0s - reasonable wait for response
|
* - 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 = {
|
export const DEFAULT_PROTOCOL_CONFIG: ProtocolConfig = {
|
||||||
cmdType: CMD_TYPE,
|
cmdType: CMD_TYPE,
|
||||||
@@ -35,6 +37,8 @@ export const DEFAULT_PROTOCOL_CONFIG: ProtocolConfig = {
|
|||||||
packetDelay: 0.1,
|
packetDelay: 0.1,
|
||||||
imageInfoDelay: 0.01,
|
imageInfoDelay: 0.01,
|
||||||
packetAckTimeout: 2.0,
|
packetAckTimeout: 2.0,
|
||||||
|
maxPacketCount: 20000,
|
||||||
|
maxPayloadSize: 2 * 1024 * 1024,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user