matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
113 lines • 5.19 kB
JavaScript
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