iobroker.roborock
Version:
199 lines (172 loc) • 6.93 kB
text/typescript
import * as protobuf from "protobufjs";
import { classifyB01MapPayload } from "./B01MapPayloadClassifier";
import { B01MapData } from "./types";
import { beautify } from "./GridBeautifier";
import { MapDecryptor } from "./MapDecryptor";
import * as MapHelper from "../MapHelper";
import { ROBOROCK_PROTO_STR } from "./roborock_proto";
import { interpolate } from "./utils";
export class MapParser {
adapter: any;
private protoRoot: protobuf.Root | null = null;
private RobotMapType: protobuf.Type | null = null;
constructor(adapter: any) {
this.adapter = adapter;
try {
this.protoRoot = protobuf.parse(ROBOROCK_PROTO_STR).root;
this.RobotMapType = this.protoRoot.lookupType("SCMap.RobotMap");
} catch (e: any) {
this.adapter.rLog("System", null, "Error", undefined, undefined, `Failed to parse Proto Schema: ${e.message}`, "error");
}
}
public parse(rawData: Buffer, serial: string, model: string, duid: string, connectionType: string): B01MapData | null {
try {
const decrypted = this.smartDecrypt(rawData, serial, model, duid);
if (!decrypted) {
this.adapter.rLog(connectionType as any, duid, "Error", "B01", 301, "[MapParser] Pipeline failed to produce valid data.", "error");
return null;
}
const payload = classifyB01MapPayload(decrypted);
if (payload.variant === "protobuf") {
return this.parseProtobuf(decrypted, duid, connectionType);
}
if (payload.variant === "q10") {
return payload.q10?.mapData ?? null;
}
this.adapter.rLog(connectionType as any, duid, "Warn", "B01", 301, "[MapParser] Unsupported B01 payload format after decryption.", "warn");
return null;
} catch (e: any) {
this.adapter.rLog(connectionType as any, duid, "Error", "B01", 301, `[MapParser] Parse failed: ${e.message}`, "error");
return null;
}
}
/**
* Delegates decryption to the centralized B01MapDecryptor.
*/
private smartDecrypt(buf: Buffer, serial: string, model: string, duid: string): Buffer | null {
const localKey = this.adapter.http_api.getMatchedLocalKeys().get(duid);
return MapDecryptor.decrypt(buf, serial, model, duid, this.adapter, localKey);
}
public parseProtobuf(buffer: Buffer, duid: string, connectionType: string): B01MapData {
const mapData: B01MapData = {
sourceFormat: "protobuf",
header: { sizeX: 0, sizeY: 0, minX: 0, minY: 0, maxX: 0, maxY: 0, resolution: 0.05 },
mapGrid: Buffer.alloc(0)
};
if (!this.RobotMapType) return mapData;
try {
const object: any = this.RobotMapType.decode(buffer);
// Map Header
if (object.mapHead) {
mapData.header.sizeX = object.mapHead.sizeX || 0;
mapData.header.sizeY = object.mapHead.sizeY || 0;
mapData.header.minX = object.mapHead.minX || 0;
mapData.header.minY = object.mapHead.minY || 0;
mapData.header.maxX = object.mapHead.maxX || 0;
mapData.header.maxY = object.mapHead.maxY || 0;
mapData.header.resolution = object.mapHead.resolution || 0.05;
}
// Map Data (Grid)
if (object.mapData && object.mapData.mapData) {
let gridBytes = object.mapData.mapData;
if (gridBytes.length > 2 && ((gridBytes[0] === 0x1f && gridBytes[1] === 0x8b) || gridBytes[0] === 0x78)) {
gridBytes = MapHelper.decompress(gridBytes);
}
mapData.mapGrid = Buffer.from(gridBytes);
}
// Positions
if (object.chargeStation) {
mapData.chargerPos = { x: object.chargeStation.x, y: object.chargeStation.y, phi: object.chargeStation.phi || 0 };
}
if (object.currentPose) {
mapData.robotPos = { x: object.currentPose.x, y: object.currentPose.y, phi: object.currentPose.phi || 0 };
}
// Areas
if (object.areasInfo) {
mapData.areasInfo = object.areasInfo.map((a: any) => ({
status: a.status, type: a.type, areaIndex: a.areaIndex, points: a.points
}));
}
if (object.virtualWalls) {
mapData.virtualWalls = object.virtualWalls.map((v: any) => ({
status: v.status, type: v.type, areaIndex: v.areaIndex, points: v.points
}));
}
if (object.recmForbitZone) {
mapData.recmForbitZone = object.recmForbitZone.map((f: any) => ({
status: f.status, type: f.type, areaIndex: f.areaIndex, points: f.points
}));
}
// Rooms
if (object.roomDataInfo) {
mapData.rooms = object.roomDataInfo.map((r: any) => ({
roomId: r.roomId, roomName: r.roomName, roomTypeId: r.roomTypeId, colorId: r.colorId,
labelPos: r.roomNamePost ? { x: r.roomNamePost.x, y: r.roomNamePost.y } : undefined
}));
}
// Room Chain
if (object.roomChain) {
mapData.roomChain = object.roomChain.map((rc: any) => ({
roomId: rc.roomId,
points: rc.points ? rc.points.map((p: any) => ({ x: p.x, y: p.y, value: p.value })) : [],
door_info: rc.door_info ? rc.door_info.map((d: any) => ({
door_point: d.door_point ? d.door_point.map((dp: any) => ({ x: dp.x, y: dp.y })) : [],
area_id: d.area_id
})) : []
}));
}
// History
if (object.historyPose && object.historyPose.points) {
mapData.history = object.historyPose.points.map((p: any) => ({ update: p.update, x: p.x, y: p.y }));
}
// Carpet
if (object.carpetInfo) {
mapData.carpetInfo = object.carpetInfo.map((c: any) => ({ id: c.id, points: c.points }));
}
this.beautifyMap(mapData);
} catch (e: any) {
const level = connectionType === "B01History" ? "debug" : "warn";
if (level === "warn") {
this.adapter.rLog(connectionType as any, duid, "Warn", "B01", 301, `Protobuf decode failure: ${e.message}`, "warn");
}
}
return mapData;
}
public beautifyMap(mapData: B01MapData) {
if (!mapData.mapGrid || mapData.mapGrid.length === 0) return;
const tMapStruct = {
map: new Int8Array(mapData.mapGrid),
x_min: mapData.header.minX,
x_max: mapData.header.maxX,
y_min: mapData.header.minY,
y_max: mapData.header.maxY,
resolution: mapData.header.resolution,
info: mapData.roomChain ? mapData.roomChain.map(rc => {
const rInfo = mapData.rooms ? mapData.rooms.find(r => r.roomId === rc.roomId) : null;
let center = { x: 0, y: 0 };
if (rInfo && rInfo.labelPos) {
center = { x: rInfo.labelPos.x, y: rInfo.labelPos.y };
} else if (rc.points && rc.points.length > 0) {
center = {
x: mapData.header.minX + (rc.points[0].x * mapData.header.resolution),
y: mapData.header.minY + (rc.points[0].y * mapData.header.resolution)
};
}
const rawChain = rc.points ? rc.points.map(p => ({ chain_point: { x: p.x, y: p.y } })) : [];
return {
tid: rc.roomId,
center_pose: center,
chain_infor: interpolate(rawChain),
door_info: rc.door_info ? rc.door_info.map(d => ({
door_point: d.door_point ? d.door_point.map(dp => ({ x: dp.x, y: dp.y })) : [],
area_id: d.area_id || []
})) : []
};
}) : null
};
const result = beautify(tMapStruct).result;
if (result && result.length > 0) {
mapData.mapGrid = Buffer.from(result);
}
}
}