UNPKG

matterbridge-roborock-vacuum-plugin

Version:
119 lines 5.15 kB
import crypto from 'node:crypto'; import zlib from 'node:zlib'; /** * Programmatic builders for legacy V1 "rr" map binary frames, used by LegacyMapParser tests. * No live device or captured sample is required — every buffer is constructed byte-by-byte. */ export const LEGACY_BLOCK_TYPE = { CHARGER_LOCATION: 1, IMAGE: 2, ROBOT_POSITION: 8, CURRENTLY_CLEANED_BLOCKS: 11, }; /** Writes the 20-byte "rr" header. SHA1 digest is intentionally omitted — LegacyMapParser does not verify it. */ export function buildMapHeader(dataLength, headerLength = 20) { const buffer = Buffer.alloc(headerLength); buffer.writeUInt8(0x72, 0); buffer.writeUInt8(0x72, 1); buffer.writeUInt16LE(headerLength, 0x02); buffer.writeUInt32LE(dataLength, 0x04); return buffer; } export function buildPositionBlock(type, x, y, angle) { const hlength = 8; const payloadLength = angle !== undefined ? 12 : 8; const buffer = Buffer.alloc(hlength + payloadLength); buffer.writeUInt16LE(type, 0); buffer.writeUInt16LE(hlength, 2); buffer.writeUInt32LE(payloadLength, 4); buffer.writeInt32LE(x, hlength); buffer.writeInt32LE(y, hlength + 4); if (angle !== undefined) { buffer.writeInt32LE(angle, hlength + 8); } return buffer; } export function buildImageBlock(pixels, size) { const hlength = 0x1c; // 28 bytes; > 24 so segment count field is included, per LegacyMapParser.parseImageBlock const { width, height, left, top } = size; const payloadLength = width * height; const buffer = Buffer.alloc(hlength + payloadLength); const floor = pixels.floor ?? []; const obstacle = pixels.obstacle ?? []; const segmentIds = new Set(floor.map((p) => p.segId)); buffer.writeUInt16LE(LEGACY_BLOCK_TYPE.IMAGE, 0); buffer.writeUInt16LE(hlength, 2); buffer.writeUInt32LE(payloadLength, 4); buffer.writeUInt32LE(segmentIds.size, 0x08); buffer.writeInt32LE(top, 0x0c); buffer.writeInt32LE(left, 0x10); buffer.writeInt32LE(height, 0x14); buffer.writeInt32LE(width, 0x18); for (const p of obstacle) { const index = p.y * width + p.x; buffer.writeUInt8(0x01, hlength + index); // pixelType 1 = obstacle } for (const p of floor) { const index = p.y * width + p.x; const byte = 0x02 | (p.segId << 3); // pixelType 2 (any non-0/non-1) = floor; bits 7:3 = segment ID buffer.writeUInt8(byte, hlength + index); } return buffer; } export function buildCleanedBlocksBlock(segmentIds) { const headerBase = 0x0c; const hlength = headerBase + segmentIds.length; const buffer = Buffer.alloc(hlength); buffer.writeUInt16LE(LEGACY_BLOCK_TYPE.CURRENTLY_CLEANED_BLOCKS, 0); buffer.writeUInt16LE(hlength, 2); buffer.writeUInt32LE(0, 4); buffer.writeUInt16LE(segmentIds.length, 0x08); segmentIds.forEach((id, i) => buffer.writeUInt8(id, headerBase + i)); return buffer; } /** Builds a raw block of an arbitrary (non-parsed) type, to verify unknown blocks are skipped without error. */ export function buildGenericBlock(type, payloadLength = 4) { const hlength = 8; const buffer = Buffer.alloc(hlength + payloadLength); buffer.writeUInt16LE(type, 0); buffer.writeUInt16LE(hlength, 2); buffer.writeUInt32LE(payloadLength, 4); return buffer; } /** Builds a minimal valid V1 "rr" map binary with the requested blocks. */ export function buildLegacyMapBuffer(options) { const blocks = []; if (options.robotPosition) { blocks.push(buildPositionBlock(LEGACY_BLOCK_TYPE.ROBOT_POSITION, options.robotPosition.x, options.robotPosition.y, options.robotPosition.angle)); } if (options.chargerPosition) { blocks.push(buildPositionBlock(LEGACY_BLOCK_TYPE.CHARGER_LOCATION, options.chargerPosition.x, options.chargerPosition.y, options.chargerPosition.angle)); } if (options.imagePixels) { const size = options.imageSize ?? { width: 10, height: 10, left: 0, top: 0 }; blocks.push(buildImageBlock(options.imagePixels, size)); } if (options.cleanedBlocks) { blocks.push(buildCleanedBlocksBlock(options.cleanedBlocks)); } if (options.unknownBlocks) { blocks.push(...options.unknownBlocks); } const dataBuffer = Buffer.concat(blocks); const header = buildMapHeader(dataBuffer.length); return Buffer.concat([header, dataBuffer]); } /** * Builds the inner-encrypted V1 map push payload as sent on Protocol 301: gzip-compress the * "rr" binary, AES-128-CBC encrypt (key=sessionNonce, IV=zeros), then prepend a 24-byte envelope. * Mirrors the device's wire format so decryptAndUnzipV1Map can round-trip it in tests. */ export function buildEncryptedV1MapPayload(rrBinary, sessionNonce) { const gzipped = zlib.gzipSync(rrBinary); const iv = Buffer.alloc(16, 0); const cipher = crypto.createCipheriv('aes-128-cbc', sessionNonce, iv); const encrypted = Buffer.concat([cipher.update(gzipped), cipher.final()]); const envelope = Buffer.alloc(24, 0); return Buffer.concat([envelope, encrypted]); } //# sourceMappingURL=legacyMapFixture.js.map