UNPKG

iobroker.roborock

Version:
1,072 lines (987 loc) 32.7 kB
// src/lib/map/v1/MapParser.ts import * as crypto from "node:crypto"; import type { Roborock } from "../../../main"; // -------------------- // Constants // -------------------- const TYPES = { CHARGER_LOCATION: 1, IMAGE: 2, PATH: 3, GOTO_PATH: 4, GOTO_PREDICTED_PATH: 5, CURRENTLY_CLEANED_ZONES: 6, GOTO_TARGET: 7, ROBOT_POSITION: 8, FORBIDDEN_ZONES: 9, VIRTUAL_WALLS: 10, CURRENTLY_CLEANED_BLOCKS: 11, NO_MOP_ZONE: 12, OBSTACLES: 13, IGNORED_OBSTACLES: 14, OBSTACLES2: 15, IGNORED_OBSTACLES2: 16, CARPET_MAP: 17, MOP_PATH: 18, CARPET_FORBIDDEN_ZONE: 19, SMART_ZONE_PATH_TYPE: 20, SMART_ZONE: 21, CUSTOM_CARPET: 22, CL_FORBIDDEN_ZONES: 23, FLOOR_MAP: 24, FURNITURES: 25, DOCK_TYPE: 26, ENEMIES: 27, DS_FORBIDDEN_ZONES: 28, STUCK_POINTS: 29, CLF_FORBIDDEN_ZONES: 30, SMART_DS: 31, FLOOR_DIRECTION: 32, DATE: 33, NONCEDATA: 34, EXT_ZONES: 36, PATROL: 37, PET_PATROL: 38, MODE_CARPET: 39, STROY_PT: 41, DIRTY_RECT: 42, IGNORE_DIRTY_RECT: 43, BRUSH_PT: 44, DIRTY_NEW: 45, MOP_ERR_PT: 46, ERAZER_ZONE: 47, LONG_CARPET: 48, DS_SIDES: 49, STEERING_PT: 50, SENSOR_INFO: 51, LOW_SPACES: 52, TIDY_ZONES: 53, GARBAGE: 54, ZONE_LINES: 55, DIGEST: 1024, /** Not in app workermapparser.jx (gap between 34 and 36). */ UNKNOWN_35: 35, /** Not in app workermapparser.jx (gap between 39 and 41). */ UNKNOWN_40: 40, /** Not in app workermapparser.jx (gap between 55 and 57). */ UNKNOWN_56: 56, /** Block 59: curtain zones (门帘). From app workermapparser.jx type "curtain". */ CURTAIN: 59, /** Block 60: missed-cleaning areas (漏扫的区域). From app workermapparser.jx type "missZone". */ MISS_ZONE: 60, }; const TYPES_REVERSE = Object.fromEntries(Object.entries(TYPES).map(([key, value]) => [value, key])); const OFFSETS = { HLENGTH: 0x02, LENGTH: 0x04, TYPE_COUNT: 0x08, TARGET_X: 0x08, ANGLE: 0x10, PATH: 0x14, TARGET_Y: 0x0a, BLOCKS: 0x0c, }; // -------------------- // Interfaces // -------------------- interface MapMetaData { header_length: number; data_length: number; version: { major: number; minor: number }; map_index: number; map_sequence: number; SHA1: string; expectedSHA1: string; } interface PositionBlock { position: [number, number]; angle: number; } interface SegmentInfo { id: number; // The segment ID (e.g., 16) name: string; // The room name (e.g., "Kitchen") center: [number, number]; // The calculated center coordinates in MM } interface ImageBlock { segments: { count: number; list: SegmentInfo[]; }; position: { top: number; left: number }; dimensions: { height: number; width: number }; pixels: { floor: number[]; obstacle: number[]; segments: number[] }; } interface PathBlock { current_angle: number; points: [number, number][]; } type Obstacle = [number, number, number, number, number, number, string]; interface NonceData { type: number; unixTime: number; } export interface ParsedMapData { metaData: MapMetaData; ROBOT_POSITION?: PositionBlock; CHARGER_LOCATION?: PositionBlock; IMAGE?: ImageBlock; PATH?: PathBlock; GOTO_PATH?: PathBlock; GOTO_PREDICTED_PATH?: PathBlock; CURRENTLY_CLEANED_ZONES?: number[][]; GOTO_TARGET?: [number, number]; VIRTUAL_WALLS?: number[][]; CURRENTLY_CLEANED_BLOCKS?: number[]; FORBIDDEN_ZONES?: number[][]; NO_MOP_ZONE?: number[][]; OBSTACLES2?: Obstacle[]; CARPET_MAP?: number[]; MOP_PATH?: number[]; CARPET_FORBIDDEN_ZONE?: number[][]; DS_FORBIDDEN_ZONES?: number[][]; CLF_FORBIDDEN_ZONES?: number[][]; MODE_CARPET?: number[][]; NONCEDATA?: NonceData[]; STROY_PT?: any[]; DIRTY_RECT?: any[]; IGNORE_DIRTY_RECT?: any[]; BRUSH_PT?: any[]; DIRTY_NEW?: any[]; MOP_ERR_PT?: any[]; ERAZER_ZONE?: any[]; LONG_CARPET?: any[]; DS_SIDES?: any[]; STEERING_PT?: any[]; SENSOR_INFO?: any[]; LOW_SPACES?: any[]; TIDY_ZONES?: any[]; GARBAGE?: any[]; ZONE_LINES?: any[]; OBSTACLES?: any[]; IGNORED_OBSTACLES?: any[]; IGNORED_OBSTACLES2?: any[]; SMART_ZONE_PATH_TYPE?: number; SMART_ZONE?: any[]; CUSTOM_CARPET?: any[]; FLOOR_MAP?: number[]; FURNITURES?: any[]; DOCK_TYPE?: number; ENEMIES?: any[]; STUCK_POINTS?: any[]; SMART_DS?: any[]; FLOOR_DIRECTION?: any[]; DATE?: number; EXT_ZONES?: any[]; PATROL?: any[]; PET_PATROL?: any[]; } export class MapParser { adapter: Roborock; constructor(adapter: Roborock) { this.adapter = adapter; } /** * Parses the raw map data from V1 Roborock vacuums. * @see test/unit/v1_map_specification.test.ts for the binary format specification. */ async parsedata(buf: Buffer, mappedRooms: any[] | null, options: { isHistoryMap?: boolean; duid?: string } = { isHistoryMap: false }): Promise<ParsedMapData | {}> { if (buf.length < 8) { this.adapter.rLog("MapManager", null, "Warn", "1.0", undefined, `Received map buffer is too small (< 8 bytes). Length: ${buf.length}`, "warn"); return {}; } let metaData: MapMetaData; let dataPosition = 0; let dataLength = buf.length; if (buf.length >= 20 && buf[0x00] === 0x72 && buf[0x01] === 0x72) { metaData = this.parseHeader(buf); if (!metaData.header_length) { this.adapter.rLog("MapManager", null, "Error", "1.0", undefined, "Failed to parse LIVE map header (Invalid structure).", "error"); return {}; } if (metaData.SHA1 !== metaData.expectedSHA1) { this.adapter.rLog("MapManager", null, "Error", "1.0", undefined, "Invalid map hash!", "error"); return {}; } dataPosition = 0x14; // Skip 20-byte header dataLength = metaData.data_length; } else if (options.isHistoryMap) { this.adapter.rLog("MapManager", null, "Debug", "1.0", undefined, "Parsing as History Map (No 'rr' Header).", "debug"); metaData = { map_index: -1 } as any; dataPosition = 0; dataLength = buf.length; } else { this.adapter.rLog("MapManager", null, "Warn", "1.0", undefined, "Invalid map header signature (expected 'rr').", "warn"); return {}; } const result: any = { metaData }; const roomIDsAll = options.duid && this.adapter.http_api.isSharedDevice(options.duid) ? await this.adapter.http_api.getSharedDeviceRooms(options.duid) : this.adapter.http_api.getMatchedRoomIDs(false); // Loop through all blocks while (dataPosition < dataLength) { if (dataPosition + OFFSETS.LENGTH + 4 > buf.length) { this.adapter.rLog("MapManager", null, "Warn", "1.0", undefined, "Reached end of buffer prematurely while reading block header.", "warn"); break; } const type = buf.readUInt16LE(dataPosition); const hlength = buf.readUInt16LE(dataPosition + OFFSETS.HLENGTH); const length = buf.readUInt32LE(dataPosition + OFFSETS.LENGTH); if (dataPosition + hlength + length > buf.length) { this.adapter.rLog("MapManager", null, "Warn", "1.0", undefined, `Block (Type ${type}) claims to be larger than buffer. Stopping parse.`, "warn"); break; } const blockBuffer = buf.subarray(dataPosition, dataPosition + hlength + length); const [offset1, offset2] = this.getTwoByteOffsets(blockBuffer); const typeName = TYPES_REVERSE[type]; if (typeName) { try { switch (type) { case TYPES.ROBOT_POSITION: case TYPES.CHARGER_LOCATION: { const position = this.getXYPositions(blockBuffer, offset1, offset2); const angle = length >= 12 ? this.getAngle(blockBuffer) : 0; result[typeName] = { position, angle }; break; } case TYPES.IMAGE: { result[typeName] = this.parseImageBlock(blockBuffer, buf, dataPosition, length, hlength, mappedRooms, roomIDsAll); break; } case TYPES.CARPET_MAP: { const carpets: number[] = []; const dataStart = dataPosition + offset1; for (let i = 0; i < length; i++) { if (this.getPixelType(buf, dataStart + i) === 1) { carpets.push(i); } } result[typeName] = carpets; break; } case TYPES.MOP_PATH: { const mopPath: number[] = []; const dataStart = dataPosition + hlength; for (let i = 0; i < length; i++) { mopPath.push(...this.readUInt8(buf, dataStart + i, 0, 1)); } result[typeName] = mopPath; break; } case TYPES.PATH: case TYPES.GOTO_PATH: case TYPES.GOTO_PREDICTED_PATH: { result[typeName] = this.parsePathBlock(blockBuffer, buf, dataPosition, length); break; } case TYPES.GOTO_TARGET: result[typeName] = this.getGoToTarget(blockBuffer); break; case TYPES.CURRENTLY_CLEANED_ZONES: case TYPES.VIRTUAL_WALLS: { const count = this.getCount(blockBuffer); const zones: number[][] = []; const dataStart = dataPosition + hlength; for (let i = 0; i < count; i++) { zones.push(this.readUInt16LE(buf, dataStart + i * 8, 0, 4)); } result[typeName] = zones; break; } case TYPES.FORBIDDEN_ZONES: case TYPES.NO_MOP_ZONE: case TYPES.CARPET_FORBIDDEN_ZONE: case TYPES.DS_FORBIDDEN_ZONES: case TYPES.CLF_FORBIDDEN_ZONES: case TYPES.MODE_CARPET: { const count = this.getCount(blockBuffer); const zones: number[][] = []; const dataStart = dataPosition + hlength; for (let i = 0; i < count; i++) { zones.push(this.getForbiddenZone(buf, dataStart + i * 16, 0)); } result[typeName] = zones; break; } case TYPES.OBSTACLES2: result[typeName] = this.extractObstacles(blockBuffer, hlength); break; case TYPES.CURRENTLY_CLEANED_BLOCKS: { const count = this.getCount(blockBuffer); const blocks: number[] = []; for (let i = 0; i < count; i++) { blocks.push(buf.readUInt8(dataPosition + OFFSETS.BLOCKS + i)); } result[typeName] = blocks; break; } case TYPES.NONCEDATA: result[typeName] = this.getNonceData(blockBuffer); break; case TYPES.STROY_PT: result[typeName] = this.getStroyPt(blockBuffer, hlength); break; case TYPES.DIRTY_RECT: case TYPES.IGNORE_DIRTY_RECT: result[typeName] = this.getDirtyRect(blockBuffer, hlength); break; case TYPES.BRUSH_PT: result[typeName] = this.getBrushPt(blockBuffer, hlength); break; case TYPES.DIRTY_NEW: result[typeName] = this.getDirtyNew(blockBuffer, hlength, length); break; case TYPES.MOP_ERR_PT: result[typeName] = this.getMopErrPt(blockBuffer, hlength); break; case TYPES.ERAZER_ZONE: case TYPES.LOW_SPACES: result[typeName] = this.getEraserZone(blockBuffer, hlength, length); break; case TYPES.LONG_CARPET: result[typeName] = this.getLongCarpet(blockBuffer, hlength, length); break; case TYPES.DS_SIDES: result[typeName] = this.getDsSides(blockBuffer, hlength, length); break; case TYPES.STEERING_PT: result[typeName] = this.getSteeringPt(blockBuffer, hlength, length); break; case TYPES.SENSOR_INFO: result[typeName] = this.getSensorInfo(blockBuffer, hlength, length); break; case TYPES.TIDY_ZONES: result[typeName] = this.getTidyZones(blockBuffer, hlength, length); break; case TYPES.GARBAGE: result[typeName] = this.getGarbage(blockBuffer, hlength, length); break; case TYPES.ZONE_LINES: result[typeName] = this.getZoneLines(blockBuffer, hlength); break; case TYPES.OBSTACLES: case TYPES.IGNORED_OBSTACLES: result[typeName] = this.getObstaclesOld(blockBuffer, hlength); break; case TYPES.IGNORED_OBSTACLES2: result[typeName] = this.getIgnoredObstacles2(blockBuffer, hlength); break; case TYPES.SMART_ZONE_PATH_TYPE: result[typeName] = buf.readUInt8(dataPosition + offset1); break; case TYPES.SMART_ZONE: result[typeName] = this.getSmartZone(blockBuffer, hlength); break; case TYPES.CUSTOM_CARPET: case TYPES.CL_FORBIDDEN_ZONES: case TYPES.SMART_DS: case TYPES.EXT_ZONES: result[typeName] = this.getForbiddenZone(buf, dataPosition + hlength, 0); // Re-use getForbiddenZone as structure is same (16 bytes) break; case TYPES.FLOOR_MAP: result[typeName] = this.readUInt8(buf, dataPosition, hlength, length); break; case TYPES.FURNITURES: result[typeName] = this.getFurnitures(blockBuffer, hlength); break; case TYPES.DOCK_TYPE: result[typeName] = buf.readUInt8(dataPosition + offset1); break; case TYPES.ENEMIES: case TYPES.STUCK_POINTS: result[typeName] = this.getEnemies(blockBuffer, hlength); break; case TYPES.FLOOR_DIRECTION: result[typeName] = this.getFloorDirection(blockBuffer, hlength, length); break; case TYPES.DATE: result[typeName] = buf.readUInt32LE(dataPosition + offset1); break; case TYPES.PATROL: case TYPES.PET_PATROL: result[typeName] = this.getPatrol(blockBuffer, hlength); break; case TYPES.CURTAIN: { const count = this.getCount(blockBuffer); const zones: number[][] = []; const dataStart = dataPosition + hlength; const entryLen = 20; // 8 coords (16 B) + status (2 B) + count (2 B) for (let i = 0; i < count && dataStart + (i + 1) * entryLen <= dataPosition + hlength + length; i++) { zones.push(this.readUInt16LE(buf, dataStart + i * entryLen, 0, 8)); } result[typeName] = zones; break; } case TYPES.MISS_ZONE: { const count = this.getCount(blockBuffer); const zones: number[][] = []; const dataStart = dataPosition + hlength; const payloadLen = length; const entryLen = count > 0 ? Math.floor(payloadLen / count) : 0; // 29 or 31 for (let i = 0; i < count && entryLen >= 29; i++) { zones.push(this.readUInt16LE(buf, dataStart + i * entryLen + 12, 0, 8)); } result[typeName] = zones; break; } case TYPES.UNKNOWN_35: case TYPES.UNKNOWN_40: case TYPES.UNKNOWN_56: this.adapter.rLog("MapManager", null, "Info", "1.0", undefined, `Unknown map block (report for analysis): type=${type} length=${length} hex=${blockBuffer.toString("hex")}`, "info"); break; } } catch (e: any) { this.adapter.rLog("MapManager", null, "Error", "1.0", undefined, `Error parsing block ${typeName} (Type ${type}): ${e.stack}`, "error"); } } else { this.adapter.rLog("MapManager", null, "Warn", "1.0", undefined, `Unknown map block (report for analysis): type=${type} length=${length} hex=${blockBuffer.toString("hex")}`, "warn"); } dataPosition += length + hlength; } return result; } private parseHeader(mapBuf: Buffer): MapMetaData { return { header_length: mapBuf.readUInt16LE(OFFSETS.HLENGTH), data_length: mapBuf.readUInt32LE(OFFSETS.LENGTH), version: { major: mapBuf.readUInt16LE(0x08), minor: mapBuf.readUInt16LE(0x0a), }, map_index: mapBuf.readUInt32LE(0x0c), map_sequence: mapBuf.readUInt32LE(0x10), SHA1: crypto .createHash("sha1") .update(mapBuf.subarray(0, mapBuf.length - 20)) .digest("hex"), expectedSHA1: mapBuf.subarray(mapBuf.length - 20).toString("hex"), }; } private parseImageBlock( blockBuffer: Buffer, buf: Buffer, dataPosition: number, length: number, hlength: number, mappedRooms: any[] | null, roomIDsAll: { id: number; name: string }[] ): ImageBlock { // MM per pixel (5cm) const MM_PER_PIXEL = 50; // Get unscaled pixel dimensions and offsets const offset = this.getSingleByteOffset(blockBuffer); const { left, top, width: width_px, height: height_px } = this.getMapSizes(blockBuffer, offset); const parameters: ImageBlock = { segments: { count: hlength > 24 ? this.getCount(blockBuffer) : 0, list: [], }, position: { top, left }, dimensions: { height: height_px, width: width_px }, pixels: { floor: [], obstacle: [], segments: [] }, }; if (height_px <= 0 || width_px <= 0) return parameters; // Create Bounding Boxes for segments const segBB: Record<number, { minX: number; maxX: number; minY: number; maxY: number; count: number }> = {}; const segmentIDsInImage: Set<number> = new Set(); const dataStart = dataPosition + offset; for (let i = 0; i < length; i++) { const pixelBytePosition = dataStart + i; const pixelType = this.getPixelType(buf, pixelBytePosition); if (pixelType === 1) { // Obstacle parameters.pixels.obstacle.push(i); } else if (pixelType !== 0) { // Floor parameters.pixels.floor.push(i); const segmentID = (buf.readUInt8(pixelBytePosition) & 248) >> 3; segmentIDsInImage.add(segmentID); parameters.pixels.segments.push(i | (segmentID << 21)); // Calculate UN-SCALED pixel coordinates relative to the data block (0, 0) const x = i % width_px; const y = Math.floor(i / width_px); const bb = segBB[segmentID]; if (!bb) { segBB[segmentID] = { minX: x, maxX: x, minY: y, maxY: y, count: 1 }; } else { if (x < bb.minX) bb.minX = x; if (x > bb.maxX) bb.maxX = x; if (y < bb.minY) bb.minY = y; if (y > bb.maxY) bb.maxY = y; bb.count++; } } } // --- Process all found segments --- for (const segId of segmentIDsInImage) { if (segId === 0) continue; // Skip "no segment" const bb = segBB[segId]; if (!bb) continue; const centerX_px = Math.round((bb.minX + bb.maxX) / 2); const centerY_px = Math.round((bb.minY + bb.maxY) / 2); // This logic MUST match the working localCoordsToRobotCoords formula // x_robot = (center_x_px + offset_x_px) * MM_PER_PIXEL // y_robot = ((height_px / scale) + top_px - center_y_px) * MM_PER_PIXEL const centerX_robot = Math.round((centerX_px + left) * MM_PER_PIXEL); // NOTE: The map creator scales dimensions.height by map_scale, which cancels out: // (height_px * scale / scale) = height_px const centerY_robot = Math.round((centerY_px + top) * MM_PER_PIXEL); let roomName = ""; const mapping = mappedRooms?.find(([id]) => parseInt(id) === segId); if (mapping) { const roomApiID = mapping[1]; const roomObj = roomIDsAll.find((r) => String(r.id) === String(roomApiID)); roomName = roomObj?.name || ""; } parameters.segments.list.push({ id: segId, name: roomName, center: [centerX_robot, centerY_robot], // Store correct MM coordinates }); } return parameters; } private parsePathBlock(blockBuffer: Buffer, buf: Buffer, dataPosition: number, length: number): PathBlock { const pathData: PathBlock = { current_angle: this.getAngle(blockBuffer), points: [], }; const pathDataPosition = dataPosition + OFFSETS.PATH; for (let i = 0; i < length; i += 4) { pathData.points.push(this.getPointInPath(buf, pathDataPosition + i)); } if (pathData.points.length >= 2) { const last = pathData.points[pathData.points.length - 1]; const secondLast = pathData.points[pathData.points.length - 2]; pathData.current_angle = (Math.atan2(last[1] - secondLast[1], last[0] - secondLast[0]) * 180) / Math.PI; } return pathData; } private extractObstacles(buf: Buffer, offset: number): Obstacle[] { const obstacleCount = this.getCount(buf); const obstacles: Obstacle[] = []; for (let i = 0; i < obstacleCount * 28; i += 28) { const obstacle: Obstacle = [ buf.readUInt16LE(offset + i), buf.readUInt16LE(offset + i + 2), buf.readUInt16LE(offset + i + 4), buf.readUInt16LE(offset + i + 6), buf.readUInt16LE(offset + i + 8), buf.readUInt16LE(offset + i + 10), buf.toString("utf-8", offset + i + 12, offset + i + 12 + 16), ]; obstacles.push(obstacle); } return obstacles; } private getStroyPt(buf: Buffer, offset: number): any[] { const count = this.getCount(buf); const points: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * 8; points.push([ buf.readUInt16LE(base), // x buf.readUInt16LE(base + 2), // y buf.readUInt32LE(base + 4) // code ]); } return points; } private getDirtyRect(buf: Buffer, offset: number): any[] { const count = this.getCount(buf); const data: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * 18; data.push([ buf.readUInt16LE(base), // type buf.readUInt16LE(base + 2), // x1 buf.readUInt16LE(base + 4), // y1 buf.readUInt16LE(base + 6), // x2 buf.readUInt16LE(base + 8), // y2 buf.readUInt16LE(base + 10), // x3 buf.readUInt16LE(base + 12), // y3 buf.readUInt16LE(base + 14), // x4 buf.readUInt16LE(base + 16) // y4 ]); } return data; } private getBrushPt(buf: Buffer, offset: number): any[] { const count = this.getCount(buf); const points: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * 4; points.push([ buf.readUInt16LE(base), // x buf.readUInt16LE(base + 2) // y ]); } return points; } private getDirtyNew(buf: Buffer, offset: number, length: number): any[] { const count = this.getCount(buf); if (count === 0) return []; const len = length / count; const data: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * len; const hasImage = buf.readUInt8(base + 18) !== 0; data.push([ buf.readUInt16LE(base), // type buf.readUInt16LE(base + 2), // x1 buf.readUInt16LE(base + 4), // y1 buf.readUInt16LE(base + 6), // x2 buf.readUInt16LE(base + 8), // y2 buf.readUInt16LE(base + 10), // x3 buf.readUInt16LE(base + 12), // y3 buf.readUInt16LE(base + 14), // x4 buf.readUInt16LE(base + 16), // y4 hasImage ? buf.toString("ascii", base + 18, base + 18 + 16).replace(/\0/g, "") : "" // imageid ]); } return data; } private getMopErrPt(buf: Buffer, offset: number): any[] { const count = this.getCount(buf); const points: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * 6; points.push([ buf.readUInt8(base), // errorid buf.readUInt8(base + 1), // suberrorid buf.readUInt16LE(base + 2), // x buf.readUInt16LE(base + 4) // y ]); } return points; } private getEraserZone(buf: Buffer, offset: number, length: number): any[] { const count = this.getCount(buf); if (count === 0) return []; const len = length / count; const data: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * len; data.push([ buf.readUInt32LE(base), // type buf.readUInt16LE(base + 4), // x1 buf.readUInt16LE(base + 6), // y1 buf.readUInt16LE(base + 8), // x2 buf.readUInt16LE(base + 10), // y2 buf.readUInt16LE(base + 12), // x3 buf.readUInt16LE(base + 14), // y3 buf.readUInt16LE(base + 16), // x4 buf.readUInt16LE(base + 18) // y4 ]); } return data; } private getLongCarpet(buf: Buffer, offset: number, length: number): any[] { const count = this.getCount(buf); if (count === 0) return []; const len = length / count; const data: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * len; data.push([ buf.readUInt32LE(base), // id buf.readUInt32LE(base + 4), // total buf.readUInt32LE(base + 8) // longhaired ]); } return data; } private getDsSides(buf: Buffer, offset: number, length: number): any[] { const count = this.getCount(buf); if (count === 0) return []; const len = length / count; const data: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * len; data.push([ buf.readUInt8(base), // id buf.readUInt8(base + 1), buf.readUInt8(base + 2), buf.readUInt8(base + 3) ]); } return data; } private getSteeringPt(buf: Buffer, offset: number, length: number): any[] { const count = this.getCount(buf); if (count === 0) return []; const len = length / count; const data: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * len; data.push([ buf.readUInt16LE(base), // x buf.readUInt16LE(base + 2), // y buf.readUInt8(base + 4) // type ]); } return data; } private getSensorInfo(buf: Buffer, offset: number, length: number): any[] { const count = this.getCount(buf); if (count === 0) return []; const len = length / count; const data: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * len; data.push([ buf.readUInt16LE(base), // x buf.readUInt16LE(base + 2), // y buf.readUInt8(base + 4), // type buf.readUInt8(base + 5) // status ]); } return data; } private getTidyZones(buf: Buffer, offset: number, length: number): any[] { const count = this.getCount(buf); if (count === 0) return []; const len = length / count; const data: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * len; data.push([ buf.readUInt16LE(base), // id buf.readUInt16LE(base + 2), // type buf.readUInt16LE(base + 4), // x1 buf.readUInt16LE(base + 6), // y1 buf.readUInt16LE(base + 8), // x2 buf.readUInt16LE(base + 10), // y2 buf.readUInt16LE(base + 12), // x3 buf.readUInt16LE(base + 14), // y3 buf.readUInt16LE(base + 16), // x4 buf.readUInt16LE(base + 18) // y4 ]); } return data; } private getGarbage(buf: Buffer, offset: number, length: number): any[] { const count = this.getCount(buf); if (count === 0) return []; const len = length / count; const data: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * len; data.push([ buf.readUInt16LE(base), // robot x buf.readUInt16LE(base + 2), // robot y buf.readUInt16LE(base + 4), // garbage x buf.readUInt16LE(base + 6), // garbage y buf.readUInt16LE(base + 8), // tidyzone id buf.readUInt16LE(base + 10), // garbage id buf.toString("utf-8", base + 12, base + len) // tag ]); } return data; } private getZoneLines(buf: Buffer, offset: number): any[] { const count = this.getCount(buf); const data: any[] = []; let toffset = 0; for (let i = 0; i < count; i++) { const base = offset + toffset; const id = buf.readUInt16LE(base); const num = buf.readUInt16LE(base + 2); const ptLen = buf.readUInt16LE(base + 4); const points: any[] = []; for (let j = 0; j < num; j++) { const pOff = base + 6 + j * ptLen; points.push([ buf.readUInt16LE(pOff), // x buf.readUInt16LE(pOff + 2), // y buf.readUIntLE(pOff + 4, ptLen - 4) // type ]); } toffset += 6 + num * ptLen; data.push({ id, num, ptLen, points }); } return data; } private getObstaclesOld(buf: Buffer, offset: number): any[] { const count = this.getCount(buf); const obstacles: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * 5; obstacles.push([ buf.readUInt16LE(base), // x buf.readUInt16LE(base + 2), // y buf.readUInt8(base + 4) // type ]); } return obstacles; } private getIgnoredObstacles2(buf: Buffer, offset: number): any[] { const count = this.getCount(buf); const obstacles: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * 6; obstacles.push([ buf.readUInt16LE(base), // x buf.readUInt16LE(base + 2), // y buf.readUInt16LE(base + 4) // type ]); } return obstacles; } private getSmartZone(buf: Buffer, offset: number): any[] { const count = this.getCount(buf); const zones: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * 18; zones.push({ zid: buf.readUInt16LE(base), range: [ buf.readUInt16LE(base + 2), buf.readUInt16LE(base + 4), buf.readUInt16LE(base + 6), buf.readUInt16LE(base + 8) ] }); } return zones; } private getFurnitures(buf: Buffer, offset: number): any[] { const count = this.getCount(buf); const furnitures: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * 23; furnitures.push([ buf.readUInt16LE(base), // x1 buf.readUInt16LE(base + 2), // y1 buf.readUInt16LE(base + 4), // x2 buf.readUInt16LE(base + 6), // y2 buf.readUInt16LE(base + 8), // x3 buf.readUInt16LE(base + 10), // y3 buf.readUInt16LE(base + 12), // x4 buf.readUInt16LE(base + 14), // y4 buf.readUInt16LE(base + 16), // x_real buf.readUInt8(base + 18), // percent buf.readUInt8(base + 19), // type buf.readUInt8(base + 20), // subtype buf.readUInt8(base + 21), // edit buf.readUInt8(base + 22) // id ]); } return furnitures; } private getEnemies(buf: Buffer, offset: number): any[] { const count = this.getCount(buf); const enemies: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * 6; enemies.push([ buf.readUInt16LE(base), // x buf.readUInt16LE(base + 2), // y buf.readUInt16LE(base + 4) // type ]); } return enemies; } private getFloorDirection(buf: Buffer, offset: number, length: number): any[] { const count = length / 3; // No count header? Code says data.length / 3 const data: any[] = []; for (let i = 0; i < count; i++) { const base = offset + i * 3; data.push([ buf.readUInt8(base), // blockid buf.readUInt16LE(base + 1) // direction ]); } return data; } private getPatrol(buf: Buffer, offset: number): any[] { // Header structure inside data: // taskId: 4 bytes (at offset) // num: 1 byte (at offset + 4) // points: num * 278 bytes (starting at offset + 5) if (buf.length < offset + 5) { return []; } const num = buf.readUInt8(offset + 4); const points: any[] = []; const dataStart = offset + 5; const pointSize = 278; // Fixed size per point based on observation for (let i = 0; i < num; i++) { const base = dataStart + i * pointSize; // Ensure we don't read past the buffer if (base + 4 > buf.length) { this.adapter.rLog("MapManager", null, "Warn", "1.0", undefined, `[MapDataParser] getPatrol: buffer too short for point ${i + 1}/${num}`, "warn"); break; } points.push([ buf.readUInt16LE(base), // x buf.readUInt16LE(base + 2) // y ]); } return points; } // -------------------- // Binary Read Helpers // -------------------- private getXYPositions(buf: Buffer, xOffset: number, yOffset: number): [number, number] { const xPosition = buf.readInt32LE(xOffset); const yPosition = buf.readInt32LE(yOffset); return [xPosition, yPosition]; } /** Reads unscaled pixel dimensions and offsets from the image block header. */ private getMapSizes(buf: Buffer, offset: number): { left: number; top: number; width: number; height: number } { const top = buf.readInt32LE(offset - 0x10); // Unscaled Pixel Offset Y const left = buf.readInt32LE(offset - 0x0c); // Unscaled Pixel Offset X const height = buf.readInt32LE(offset - 0x08); // Unscaled Pixel Height const width = buf.readInt32LE(offset - 0x04); // Unscaled Pixel Width return { left, top, width, height }; } private getPointInPath(buf: Buffer, dataPosition: number): [number, number] { const x = buf.readUInt16LE(dataPosition); const y = buf.readUInt16LE(dataPosition + 2); return [x, y]; } private getCount(buf: Buffer): number { return buf.readUInt32LE(OFFSETS.TYPE_COUNT); } private getPixelType(buf: Buffer, dataPosition: number): number { return buf.readUInt8(dataPosition) & 0x07; } private getAngle(buf: Buffer): number { return buf.readInt32LE(OFFSETS.ANGLE); } private getGoToTarget(buf: Buffer): [number, number] { return [buf.readUInt16LE(OFFSETS.TARGET_X), buf.readUInt16LE(OFFSETS.TARGET_Y)]; } private getForbiddenZone(buf: Buffer, dataPosition: number, offset: number): number[] { return this.readUInt16LE(buf, dataPosition, offset, 8); } private getSingleByteOffset(buf: Buffer): number { return buf.readUInt8(2); } private getTwoByteOffsets(buf: Buffer): [number, number] { return [buf.readUInt8(2), buf.readUInt8(4)]; } private getNonceData(buf: Buffer): NonceData[] { const sections: NonceData[] = []; for (let i = 12; i < buf.length; i += 5) { const type = buf[i]; const unixTime = buf.readUInt32LE(i + 1); sections.push({ type, unixTime }); } return sections; } private readUInt16LE(buf: Buffer, dataPosition: number, offset: number, count: number): number[] { const result: number[] = []; for (let j = 0; j < count; j++) { result.push(buf.readUInt16LE(dataPosition + offset + j * 2)); } return result; } private readUInt8(buf: Buffer, dataPosition: number, offset: number, count: number): number[] { const array: number[] = []; for (let j = 0; j < count; j++) { array.push(buf.readUInt8(offset + dataPosition + j)); } return array; } }