UNPKG

matterbridge-roborock-vacuum-plugin

Version:
113 lines 5.19 kB
import crypto from 'node:crypto'; import zlib from 'node:zlib'; import protobuf from 'protobufjs'; import { parseQ10MapPacket } from './b01Q10MapParser.js'; import { parseQ10TracePacket } from './b01Q10TraceParser.js'; import { ROBOROCK_PROTO_STR } from './roborockProto.js'; export class B01MapParser { robotMapType; constructor() { const root = protobuf.parse(ROBOROCK_PROTO_STR).root; this.robotMapType = root.lookupType('SCMap.RobotMap'); } parseRoomsFromEncryptedBinary(rawBuffer, modelShortCode, serial) { if (this.isQ10ShapedPayload(rawBuffer)) { return this.parseQ10Binary(rawBuffer); } if (this.isTracePacket(rawBuffer)) { return this.parseTraceBinary(rawBuffer); } const decoded = this.decodeBase64IfNeeded(rawBuffer); const decrypted = this.decryptIfNeeded(decoded, modelShortCode, serial); const hexed = this.asciiHexToBinaryIfNeeded(decrypted); const decompressed = zlib.inflateSync(hexed); return this.parseRooms(decompressed); } isQ10ShapedPayload(rawBuffer) { return rawBuffer.length >= 2 && rawBuffer[0] === 0x01 && rawBuffer[1] === 0x01; } isTracePacket(rawBuffer) { return rawBuffer.length >= 2 && rawBuffer[0] === 0x02 && rawBuffer[1] === 0x01; } parseQ10Binary(rawBuffer) { try { return parseQ10MapPacket(rawBuffer); } catch (err) { throw new Error(`Q10 map binary parse failed (best-effort Q10 layout, unconfirmed against real device capture): ${String(err instanceof Error ? err.message : err)}`, { cause: err }); } } parseTraceBinary(rawBuffer) { try { return parseQ10TracePacket(rawBuffer); } catch (err) { throw new Error(`Q10 trace packet parse failed (marker 0x02 0x01, confirmed against python-roborock reference): ${String(err instanceof Error ? err.message : err)}`, { cause: err }); } } decodeBase64IfNeeded(data) { const sample = data.subarray(0, Math.min(data.length, 100)).toString('utf8'); if (/^[A-Za-z0-9+/= \r\n]+$/.test(sample)) { return Buffer.from(data.toString('utf8'), 'base64'); } return data; } decryptIfNeeded(data, modelShortCode, serial) { if (data.length % 16 !== 0) return data; const key = this.deriveEncryptionKey(modelShortCode, serial); const decipher = crypto.createDecipheriv('aes-128-ecb', key, null); decipher.setAutoPadding(true); return Buffer.concat([decipher.update(data), decipher.final()]); } asciiHexToBinaryIfNeeded(data) { const sample = data.subarray(0, 10).toString('utf8'); if (/^[0-9a-fA-F]+$/.test(sample) && sample.startsWith('78')) { return Buffer.from(data.toString('utf8'), 'hex'); } return data; } deriveEncryptionKey(modelShortCode, serial) { const baseKey = Buffer.from(modelShortCode.padEnd(16, '0'), 'utf8'); const data = Buffer.from(`${serial}+${modelShortCode}+${serial}`, 'utf8'); const padLength = 16 - (data.length % 16); const paddedData = Buffer.concat([data, Buffer.alloc(padLength, padLength)]); const cipher = crypto.createCipheriv('aes-128-ecb', baseKey, null); cipher.setAutoPadding(false); const encrypted = Buffer.concat([cipher.update(paddedData), cipher.final()]); const hash = crypto.createHash('md5').update(encrypted.toString('base64')).digest('hex'); return Buffer.from(hash.substring(8, 24).toLowerCase(), 'utf8'); } parseRooms(buffer) { const decoded = this.robotMapType.decode(buffer); const roomDataInfo = decoded.roomDataInfo; const mapHead = decoded.mapHead; const mapId = typeof mapHead?.mapHeadId === 'number' && mapHead.mapHeadId > 0 ? mapHead.mapHeadId : undefined; const currentPoseRaw = decoded.currentPose; const currentPose = currentPoseRaw && typeof currentPoseRaw.x === 'number' && typeof currentPoseRaw.y === 'number' ? { x: currentPoseRaw.x, y: currentPoseRaw.y, phi: typeof currentPoseRaw.phi === 'number' ? currentPoseRaw.phi : undefined, } : undefined; const roomMatrixRaw = decoded.roomMatrix; const matrixBytes = roomMatrixRaw?.matrix; const roomMatrix = Buffer.isBuffer(matrixBytes) && matrixBytes.length > 0 ? { data: matrixBytes } : undefined; if (!roomDataInfo || roomDataInfo.length === 0) { return { rooms: [], mapId, currentPose, roomMatrix }; } const rooms = roomDataInfo.map((r) => { const namePost = r.roomNamePost; return { roomId: r.roomId, roomName: r.roomName || '', roomTypeId: r.roomTypeId, colorId: r.colorId, labelPos: namePost ? { x: namePost.x, y: namePost.y } : undefined, }; }); return { rooms, mapId, currentPose, roomMatrix }; } } //# sourceMappingURL=b01MapParser.js.map