matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
153 lines • 6.62 kB
JavaScript
const MM_PER_PIXEL = 50;
const BLOCK_TYPE = {
CHARGER_LOCATION: 1,
IMAGE: 2,
ROBOT_POSITION: 8,
CURRENTLY_CLEANED_BLOCKS: 11,
};
export class LegacyMapParser {
parse(buffer) {
if (buffer.length < 20 || buffer[0] !== 0x72 || buffer[1] !== 0x72)
return {};
const headerLength = buffer.readUInt16LE(0x02);
const dataLength = buffer.readUInt32LE(0x04);
const dataEnd = headerLength + dataLength;
const result = {};
let dataPosition = headerLength;
while (dataPosition < dataEnd) {
if (dataPosition + 8 > buffer.length)
break;
const type = buffer.readUInt16LE(dataPosition);
const blockHLength = buffer.readUInt16LE(dataPosition + 2);
const blockDataLength = buffer.readUInt32LE(dataPosition + 4);
if (dataPosition + blockHLength + blockDataLength > buffer.length)
break;
switch (type) {
case BLOCK_TYPE.ROBOT_POSITION:
result.robotPosition = this.parsePositionBlock(buffer, dataPosition, blockHLength, blockDataLength);
break;
case BLOCK_TYPE.CHARGER_LOCATION:
result.chargerPosition = this.parsePositionBlock(buffer, dataPosition, blockHLength, blockDataLength);
break;
case BLOCK_TYPE.IMAGE:
result.image = this.parseImageBlock(buffer, dataPosition, blockHLength, blockDataLength);
break;
case BLOCK_TYPE.CURRENTLY_CLEANED_BLOCKS:
result.currentlyCleanedBlocks = this.parseCleanedBlocks(buffer, dataPosition);
break;
default:
break;
}
dataPosition += blockHLength + blockDataLength;
}
return result;
}
resolveCurrentRoom(data, namedRooms) {
if (!data.robotPosition || !data.image)
return undefined;
const { x, y } = data.robotPosition;
const { position: { top, left }, dimensions: { width, height }, pixels: { segments }, } = data.image;
const pxAbs = Math.round(x / MM_PER_PIXEL);
const pyAbs = Math.round(y / MM_PER_PIXEL);
const pxRel = pxAbs - left;
const pyRel = height - 1 - (pyAbs - top);
if (pxRel < 0 || pxRel >= width || pyRel < 0 || pyRel >= height)
return undefined;
const targetIndex = pyRel * width + pxRel;
for (const packed of segments) {
if ((packed & 0x1fffff) === targetIndex) {
const segmentId = packed >> 21;
const name = namedRooms?.find((r) => r.id === segmentId)?.name ?? '';
return { segmentId, name };
}
}
return this.findNearestSegment(data.image.segments.list, x, y, namedRooms);
}
parsePositionBlock(buffer, dataPosition, hlength, length) {
const payloadStart = dataPosition + hlength;
const x = buffer.readInt32LE(payloadStart);
const y = buffer.readInt32LE(payloadStart + 4);
const angle = length >= 12 ? buffer.readInt32LE(payloadStart + 8) : 0;
return { x, y, angle };
}
parseImageBlock(buffer, dataPosition, hlength, length) {
const segmentCount = hlength > 24 ? buffer.readUInt32LE(dataPosition + 0x08) : 0;
const top = buffer.readInt32LE(dataPosition + 0x0c);
const left = buffer.readInt32LE(dataPosition + 0x10);
const height = buffer.readInt32LE(dataPosition + 0x14);
const width = buffer.readInt32LE(dataPosition + 0x18);
const pixelStart = dataPosition + hlength;
const floor = [];
const obstacle = [];
const segments = [];
const segmentBoundingBoxes = new Map();
const segmentIdsSeen = new Set();
for (let i = 0; i < length; i++) {
const byte = buffer.readUInt8(pixelStart + i);
const pixelType = byte & 0x07; // bits 2:0
if (pixelType === 1) {
obstacle.push(i);
continue;
}
if (pixelType === 0)
continue;
floor.push(i);
const segmentId = (byte & 0xf8) >> 3; // bits 7:3
if (segmentId === 0)
continue;
segmentIdsSeen.add(segmentId);
segments.push(i | (segmentId << 21));
this.updateBoundingBox(segmentBoundingBoxes, segmentId, i % width, Math.floor(i / width));
}
const list = [...segmentIdsSeen].map((segmentId) => {
const bb = segmentBoundingBoxes.get(segmentId);
const centerX = bb ? Math.round(((bb.minX + bb.maxX) / 2 + left) * MM_PER_PIXEL) : 0;
const centerY = bb ? Math.round(((bb.minY + bb.maxY) / 2 + top) * MM_PER_PIXEL) : 0;
return { id: segmentId, name: '', center: [centerX, centerY] };
});
return {
position: { top, left },
dimensions: { width, height },
pixels: { floor, obstacle, segments },
segments: { count: segmentCount, list },
};
}
parseCleanedBlocks(buffer, dataPosition) {
const count = buffer.readUInt16LE(dataPosition + 0x08);
const blocks = [];
for (let i = 0; i < count; i++) {
blocks.push(buffer.readUInt8(dataPosition + 0x0c + i));
}
return blocks;
}
updateBoundingBox(map, segmentId, px, py) {
const existing = map.get(segmentId);
if (!existing) {
map.set(segmentId, { minX: px, maxX: px, minY: py, maxY: py });
return;
}
existing.minX = Math.min(existing.minX, px);
existing.maxX = Math.max(existing.maxX, px);
existing.minY = Math.min(existing.minY, py);
existing.maxY = Math.max(existing.maxY, py);
}
findNearestSegment(list, x, y, namedRooms) {
if (list.length === 0)
return undefined;
let closest;
let closestDistance = Infinity;
for (const segment of list) {
const [centerX, centerY] = segment.center;
const distance = Math.hypot(centerX - x, centerY - y);
if (distance < closestDistance) {
closestDistance = distance;
closest = segment;
}
}
if (!closest)
return undefined;
const name = namedRooms?.find((r) => r.id === closest.id)?.name ?? closest.name;
return { segmentId: closest.id, name };
}
}
//# sourceMappingURL=mapParser.js.map