Hallo zusammen,
ich habe dieses Skript schon mal an anderer Stelle gepostet. Der Roborock Generation 1 hat ja das Problem, dass er gerne mal die Karte dreht und somit statische Skripte nicht funktionieren, wenn man die Zonenreinigung als Einzelraumreinigung nutzen möchte.
Das Problem habe ich mit der alternativen Firmware Valetudo umgangen, bei der man anhand der Kartendaten auslesen kann, wie und ob die Karte gedreht wurde.
Die Koordinaten sind kompatibel zum mihome-vacuum Adapter und damit gegenüber den Koordinaten aus Valetudo vertikal gespiegelt. Zum Umrechnen müssen die y-Koordinaten folgendermaßen umgerechnet werden: y(skript) = 51200 - y(Valetudo)?
Version 1: funktioniert bis Firmware Valetudo 0.4.0-RE5
Muss als TypeScript angelegt werden und benötigt das zusätzliche Paket "axios":
import axios from "axios";
// Hier den Hostnamen und Zugangsdaten eintragen, unter dem Valetudo erreichbar ist
const roboHostname = "rockrobo.fritz.box";
const valetudoAuth = {
username: 'valetudo-username',
password: 'valetudo-password'
}
// Hier den State eintragen, der den aktuellen Sauger-Status angibt
const idVacuumState = "mihome-vacuum.0.info.state";
const center: Point = [25600, 25600];
// Koordiaten zählen von unten links (0,0) nach oben rechts (51200,51200)
// Die folgenden Koordinaten gehen von nicht rotierter Karte (Winkel 0) aus
const rooms: Record<string, Rectangle[]> = {
"Küche": [[21600, 29100, 23500, 33100]],
"Flur": [[18400, 24000, 22200, 27100], [19500, 27100, 21600, 30400]],
"Wohnzimmer": [[17600, 20000, 21600, 24000], [21600, 19200, 24200, 23200]],
"Schlafzimmer": [[24100, 23200, 26100, 27400], [22100, 23800, 24100, 27400]],
"Bad": [[21500, 27500, 23200, 29000]],
};
// ===============================================================================
type Rectangle = [number, number, number, number];
type Point = [number, number];
/** rotates a rectangle by 90° around the absolute origin */
function rotate90([x1, y1, x2, y2]: Rectangle): Rectangle {
return [-y2, x1, -y1, x2];
}
/** rotates a rectangle by 180° around the absolute origin */
function rotate180([x1, y1, x2, y2]: Rectangle): Rectangle {
return [-x2, -y2, -x1, -y1];
}
/** Rotates a rectangle around the given center by the given angle */
function rotate(rect: Rectangle, center: Point, angle: number) {
rect = [rect[0] - center[0], rect[1] - center[1], rect[2] - center[0], rect[3] - center[1]];
if (angle % 180 === 90) {
rect = rotate90(rect);
angle -= 90;
}
if (angle === 180) {
rect = rotate180(rect);
}
rect = [rect[0] + center[0], rect[1] + center[1], rect[2] + center[0], rect[3] + center[1]];
return rect;
}
for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) {
createState(`Staubsauger.${room}`, {
type: "boolean",
read: true,
write: true,
role: "switch",
name: `${room} saugen`,
});
on({ id: `javascript.${instance}.Staubsauger.${room}`, val: true, ack: false }, async () => {
if (getState(idVacuumState).val !== 8) await cancelCurrentAction();
if (getMapRotation() === -1 /* unbekannt */) {
// We need to test the map rotation
const rotation = await testMapRotation();
log(`Die Karte ist ${rotation !== 0 ? `um ${rotation}° ` : "nicht "}rotiert.`);
await rememberMapRotation(rotation);
}
// Now that we know how the map is rotated, we can clean the room
cleanRoom(room);
});
}
createState(`Staubsauger.stop`, {
type: "boolean",
read: true,
write: true,
role: "switch",
name: `Staubsauger anhalten`,
});
on({ id: `javascript.${instance}.Staubsauger.stop`, val: true, ack: false }, () => {
stopCleanup();
});
const idMapRotated = "Staubsauger.info.mapRotated";
createState(idMapRotated, {
type: "number",
read: true,
write: false,
role: "indicator",
states: {
"-1": "unknown",
"0": "Robo links vom Dock",
"90": "Robo unten vom Dock",
"180": "Robo rechts vom Dock",
"270": "Robo oben vom Dock",
},
name: `Wie die Karte rotiert ist`,
});
/**
* Bestimmt die Kartenrotation wenn der Staubsauger neben dem Dock steht
* 0 Grad bedeutet, der Sauger steht links
*/
async function testMapRotation(): Promise<number> {
log("Teste Kartenorientierung...");
const { data: { charger, robot } } = await axios({
url: `http://${roboHostname}/api/map/latest`,
auth: valetudoAuth
});
// Valetudo zählt von oben links nach unten rechts, d.h. die Y-Koordinaten
// sind entgegengesetzt der mathematischen Definition
let c2r = [robot[0] - charger[0], charger[1] - robot[1]];
const angle = Math.atan2(c2r[1], c2r[0]) * 180 / Math.PI;
if (angle <= 45 && angle >= -45) {
// Sauger steht rechts
return 180;
} else if (angle > 45 && angle < 135) {
// Sauger steht oben
return 270;
} else if (angle < -45 && angle > -135) {
// Sauger steht unten
return 90;
} else {
return 0;
}
}
function rememberMapRotation(rotation: number): Promise<void> {
return setStateAsync(idMapRotated, rotation);
}
function getMapRotation(): number {
return getState(idMapRotated).val;
}
// "Forget" map rotation when the vacuum starts charging
on({ id: idVacuumState, val: 8 /* charging */ }, (obj) => {
setState(idMapRotated, -1 /* unknown */);
// And reset all control states
for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) {
setState(`Staubsauger.${room}`, false, true);
}
setState(`Staubsauger.stop`, false, true);
});
async function beginCleanup(): Promise<void> {
}
async function cancelCurrentAction(): Promise<void> {
setState("mihome-vacuum.0.control.pause", true);
// wait for the "paused" status before going home
await waitForPauseOrSleep();
}
async function stopCleanup(): Promise<void> {
log(`Saugvorgang abgebrochen!`);
setState(`Staubsauger.stop`, true, true);
if (getState(idVacuumState).val !== 8) {
await cancelCurrentAction();
await setStateAsync('mihome-vacuum.0.control.home', true);
// wait for the "charging" status before resolving
await waitFor(idVacuumState, 8);
}
log(`Staubsauger ist in der Basis`);
setState(`Staubsauger.stop`, false, true);
}
async function cleanRoom(room: keyof typeof rooms): Promise<void> {
log(`Saugvorgang für ${room} gestartet!`);
setState(`Staubsauger.${room}`, true, true);
const mapRotation = getMapRotation();
const originalCoords = rooms[room];
log(`original coordinates: ${JSON.stringify(originalCoords)}`);
const roomCoords = rooms[room].map(rect => rotate(rect, center, mapRotation));
log(`rotated coordinates: ${JSON.stringify(roomCoords)}`);
const coords = roomCoords[0];
const targetCoords = [
((coords[0] + coords[2]) / 2).toFixed(0),
((coords[1] + coords[3]) / 2).toFixed(0),
]
// go to center of first zone
const gotoString = targetCoords.join(",");
await setStateAsync("mihome-vacuum.0.control.goTo", gotoString);
log(`Fahre zur Mitte von ${room}`);
await wait(10000);
await waitForPauseOrSleep();
if (getState("Staubsauger.stop").val) return;
const zoneCleanString = roomCoords.map(zone => {
return "[" + zone.concat(1).map(coord => coord.toString()).join(",") + "]";
}).join(",");
log("Starte Zonenreinigung...");
await setStateAsync("mihome-vacuum.0.control.zoneClean", zoneCleanString);
// wait for the cleanup to finish
await waitFor(idVacuumState, 6);
}
async function waitForPauseOrSleep(): Promise<void> {
log("Warte auf Zustand schlafen oder Pause...");
switch (getState(idVacuumState).val) {
case 3:
case 10:
log(" => Zustand bereits aktiv!");
return;
default:
await Promise.race([
waitFor(idVacuumState, 10),
waitFor(idVacuumState, 3),
]);
log(" => Zustand erreicht!");
}
}
function wait(ms: number): Promise<void> {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
function waitFor(stateID: string, value: any): Promise<void> {
return new Promise(resolve => {
const handler = (obj: iobJS.ChangedStateObject) => {
if (obj.newState.val === value) {
unsubscribe(handler);
resolve();
}
}
subscribe(stateID, handler);
})
}
function setStateAsync(id: string, state: any): Promise<void> {
return new Promise(res => {
setState(id, state, () => res());
});
}
Version 2: Wie Version 1, nur ohne Prüfung der automatischen Rotation. Benötigt kein Valetudo auf dem Sauger:
// Hier den State eintragen, der den aktuellen Sauger-Status angibt
const idVacuumState = "mihome-vacuum.0.info.state";
// Koordiaten zählen von unten links (0,0) nach oben rechts (51200,51200)
// Die folgenden Koordinaten gehen von nicht rotierter Karte (Winkel 0) aus
const rooms: Record<string, Rectangle[]> = {
"Küche": [[21600, 29100, 23500, 33100]],
"Flur": [[18400, 24000, 22200, 27100], [19500, 27100, 21600, 30400]],
"Wohnzimmer": [[17600, 20000, 21600, 24000], [21600, 19200, 24200, 23200]],
"Schlafzimmer": [[24100, 23200, 26100, 27400], [22100, 23800, 24100, 27400]],
"Bad": [[21500, 27500, 23200, 29000]],
};
// ===============================================================================
type Rectangle = [number, number, number, number];
for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) {
createState(`Staubsauger.${room}`, {
type: "boolean",
read: true,
write: true,
role: "switch",
name: `${room} saugen`,
});
on({ id: `javascript.${instance}.Staubsauger.${room}`, val: true, ack: false }, async () => {
if (getState(idVacuumState).val !== 8) await cancelCurrentAction();
cleanRoom(room);
});
}
createState(`Staubsauger.stop`, {
type: "boolean",
read: true,
write: true,
role: "switch",
name: `Staubsauger anhalten`,
});
on({ id: `javascript.${instance}.Staubsauger.stop`, val: true, ack: false }, () => {
stopCleanup();
});
// Reset all control states when the vacuum starts charging
on({ id: idVacuumState, val: 8 /* charging */ }, (obj) => {
for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) {
setState(`Staubsauger.${room}`, false, true);
}
setState(`Staubsauger.stop`, false, true);
});
async function cancelCurrentAction(): Promise<void> {
setState("mihome-vacuum.0.control.pause", true);
// wait for the "paused" status before going home
await waitForPauseOrSleep();
}
async function stopCleanup(): Promise<void> {
log(`Saugvorgang abgebrochen!`);
setState(`Staubsauger.stop`, true, true);
if (getState(idVacuumState).val !== 8) {
await cancelCurrentAction();
await setStateAsync('mihome-vacuum.0.control.home', true);
// wait for the "charging" status before resolving
await waitFor(idVacuumState, 8);
}
log(`Staubsauger ist in der Basis`);
setState(`Staubsauger.stop`, false, true);
}
async function cleanRoom(room: keyof typeof rooms): Promise<void> {
log(`Saugvorgang für ${room} gestartet!`);
setState(`Staubsauger.${room}`, true, true);
const roomCoords = rooms[room];
const coords = roomCoords[0];
const targetCoords = [
((coords[0] + coords[2]) / 2).toFixed(0),
((coords[1] + coords[3]) / 2).toFixed(0),
]
// go to center of first zone
const gotoString = targetCoords.join(",");
await setStateAsync("mihome-vacuum.0.control.goTo", gotoString);
log(`Fahre zur Mitte von ${room}`);
await wait(10000);
await waitForPauseOrSleep();
if (getState("Staubsauger.stop").val) return;
const zoneCleanString = roomCoords.map(zone => {
return "[" + zone.concat(1).map(coord => coord.toString()).join(",") + "]";
}).join(",");
log("Starte Zonenreinigung...");
await setStateAsync("mihome-vacuum.0.control.zoneClean", zoneCleanString);
// wait for the cleanup to finish
await waitFor(idVacuumState, 6);
}
async function waitForPauseOrSleep(): Promise<void> {
log("Warte auf Zustand schlafen oder Pause...");
switch (getState(idVacuumState).val) {
case 3:
case 10:
log(" => Zustand bereits aktiv!");
return;
default:
await Promise.race([
waitFor(idVacuumState, 10),
waitFor(idVacuumState, 3),
]);
log(" => Zustand erreicht!");
}
}
function wait(ms: number): Promise<void> {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
function waitFor(stateID: string, value: any): Promise<void> {
return new Promise(resolve => {
const handler = (obj: iobJS.ChangedStateObject) => {
if (obj.newState.val === value) {
unsubscribe(handler);
resolve();
}
}
subscribe(stateID, handler);
})
}
function setStateAsync(id: string, state: any): Promise<void> {
return new Promise(res => {
setState(id, state, () => res());
});
}
Beide Varianten legen für jeden Raum und für Stop einen State an, den du dann jeweils mit einer Alexa-Routine ansprechen kannst.
Version 3: Wie 1, funktioniert ab Firmware Valetudo 0.4.0-RE6. Zusätzlich muss das Modul "pako"
in den Adapter-Einstellungen geladen werden.
import axios from "axios";
// Hier den Hostnamen und Zugangsdaten eintragen, unter dem Valetudo erreichbar ist
const roboHostname = "rockrobo.fritz.box";
const valetudoAuth = {
username: "valetudo-username",
password: "valetudo-password"
};
// Hier den State eintragen, der den aktuellen Sauger-Status angibt
const idVacuumState = "mihome-vacuum.0.info.state";
const center: Point = [25600, 25600];
// Koordiaten zählen von unten links (0,0) nach oben rechts (51200,51200)
// Die folgenden Koordinaten gehen von nicht rotierter Karte (Winkel 0) aus
const rooms: Record<string, Rectangle[]> = {
"Küche": [[21600, 29100, 23500, 33100]],
"Flur": [[18400, 24000, 22200, 27100], [19500, 27100, 21600, 30400]],
"Wohnzimmer": [[17600, 20000, 21600, 24000], [21600, 19200, 24200, 23200]],
"Schlafzimmer": [[24100, 23200, 26100, 27400], [22100, 23800, 24100, 27400]],
"Bad": [[21500, 27500, 23200, 29000]],
};
// ===============================================================================
type Rectangle = [number, number, number, number];
type Point = [number, number];
/** rotates a rectangle by 90° around the absolute origin */
function rotate90([x1, y1, x2, y2]: Rectangle): Rectangle {
return [-y2, x1, -y1, x2];
}
/** rotates a rectangle by 180° around the absolute origin */
function rotate180([x1, y1, x2, y2]: Rectangle): Rectangle {
return [-x2, -y2, -x1, -y1];
}
/** Rotates a rectangle around the given center by the given angle */
function rotate(rect: Rectangle, center: Point, angle: number) {
rect = [rect[0] - center[0], rect[1] - center[1], rect[2] - center[0], rect[3] - center[1]];
if (angle % 180 === 90) {
rect = rotate90(rect);
angle -= 90;
}
if (angle === 180) {
rect = rotate180(rect);
}
rect = [rect[0] + center[0], rect[1] + center[1], rect[2] + center[0], rect[3] + center[1]];
return rect;
}
for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) {
createState(`Staubsauger.${room}`, {
type: "boolean",
read: true,
write: true,
role: "switch",
name: `${room} saugen`,
});
on({ id: `javascript.${instance}.Staubsauger.${room}`, val: true, ack: false }, async () => {
if (getState(idVacuumState).val !== 8) await cancelCurrentAction();
if (mapRotation === -1 /* unbekannt */) {
// We need to test the map rotation
const rotation = await testMapRotation();
log(`Die Karte ist ${rotation !== 0 ? `um ${rotation}° ` : "nicht "}rotiert.`);
await rememberMapRotation(rotation);
}
// Now that we know how the map is rotated, we can clean the room
cleanRoom(room);
});
}
createState(`Staubsauger.stop`, {
type: "boolean",
read: true,
write: true,
role: "switch",
name: `Staubsauger anhalten`,
});
on({ id: `javascript.${instance}.Staubsauger.stop`, val: true, ack: false }, () => {
stopCleanup();
});
const idMapRotated = "Staubsauger.info.mapRotated";
createState(idMapRotated, {
type: "number",
read: true,
write: false,
role: "indicator",
states: {
"-1": "unknown",
"0": "Robo links vom Dock",
"90": "Robo unten vom Dock",
"180": "Robo rechts vom Dock",
"270": "Robo oben vom Dock",
},
name: `Wie die Karte rotiert ist`,
});
let mapRotation: number = -1;
if (existsState(idMapRotated)) {
const rotation = getState(idMapRotated).val;
if (rotation != undefined) mapRotation = rotation;
}
on(idMapRotated, obj => {
mapRotation = obj.state.val;
});
/**
* Bestimmt die Kartenrotation wenn der Staubsauger neben dem Dock steht
* 0 Grad bedeutet, der Sauger steht links
*/
async function testMapRotation(): Promise<number> {
log("Teste Kartenorientierung...");
const binaryData = await axios({
url: `http://${roboHostname}/api/map/latest`,
auth: valetudoAuth,
responseType: "arraybuffer"
});
// Decode map data
let charger;
let robot;
try {
const parsed = RRMapParser.PARSEDATA(
Buffer.from(
require("pako").inflate(binaryData.data)
)
);
({ charger, robot } = parsed);
} catch (e) {
log(`Error decoding map data: ${e.message}`, "warn");
return 0;
}
// Valetudo zählt von oben links nach unten rechts, d.h. die Y-Koordinaten
// sind entgegengesetzt der mathematischen Definition
let c2r = [robot[0] - charger[0], charger[1] - robot[1]];
const angle = Math.atan2(c2r[1], c2r[0]) * 180 / Math.PI;
if (angle <= 45 && angle >= -45) {
// Sauger steht rechts
return 180;
} else if (angle > 45 && angle < 135) {
// Sauger steht oben
return 270;
} else if (angle < -45 && angle > -135) {
// Sauger steht unten
return 90;
} else {
return 0;
}
}
function rememberMapRotation(rotation: number): Promise<void> {
mapRotation = rotation;
return setStateAsync(idMapRotated, rotation);
}
// "Forget" map rotation when the vacuum starts charging
on({ id: idVacuumState, val: 8 /* charging */ }, (obj) => {
setState(idMapRotated, -1 /* unknown */);
// And reset all control states
for (const room of Object.keys(rooms) as (keyof typeof rooms)[]) {
setState(`Staubsauger.${room}`, false, true);
}
setState(`Staubsauger.stop`, false, true);
});
async function beginCleanup(): Promise<void> {
}
async function cancelCurrentAction(): Promise<void> {
setState("mihome-vacuum.0.control.pause", true);
// wait for the "paused" status before going home
await waitForPauseOrSleep();
}
async function stopCleanup(): Promise<void> {
log(`Saugvorgang abgebrochen!`);
setState(`Staubsauger.stop`, true, true);
if (getState(idVacuumState).val !== 8) {
await cancelCurrentAction();
await setStateAsync('mihome-vacuum.0.control.home', true);
// wait for the "charging" status before resolving
await waitFor(idVacuumState, 8);
}
log(`Staubsauger ist in der Basis`);
setState(`Staubsauger.stop`, false, true);
}
async function cleanRoom(room: keyof typeof rooms): Promise<void> {
log(`Saugvorgang für ${room} gestartet!`);
setState(`Staubsauger.${room}`, true, true);
const originalCoords = rooms[room];
log(`original coordinates: ${JSON.stringify(originalCoords)}`);
const roomCoords = rooms[room].map(rect => rotate(rect, center, mapRotation));
log(`rotated coordinates: ${JSON.stringify(roomCoords)}`);
const coords = roomCoords[0];
const targetCoords = [
((coords[0] + coords[2]) / 2).toFixed(0),
((coords[1] + coords[3]) / 2).toFixed(0),
]
// go to center of first zone
const gotoString = targetCoords.join(",");
await setStateAsync("mihome-vacuum.0.control.goTo", gotoString);
log(`Fahre zur Mitte von ${room}`);
await wait(10000);
await waitForPauseOrSleep();
if (getState("Staubsauger.stop").val) return;
const zoneCleanString = roomCoords.map(zone => {
return "[" + zone.concat(1).map(coord => coord.toString()).join(",") + "]";
}).join(",");
log("Starte Zonenreinigung...");
await setStateAsync("mihome-vacuum.0.control.zoneClean", zoneCleanString);
// wait for the cleanup to finish
await waitFor(idVacuumState, 6);
}
async function waitForPauseOrSleep(): Promise<void> {
log("Warte auf Zustand schlafen oder Pause...");
switch (getState(idVacuumState).val) {
case 3:
case 10:
log(" => Zustand bereits aktiv!");
return;
default:
await Promise.race([
waitFor(idVacuumState, 10),
waitFor(idVacuumState, 3),
]);
log(" => Zustand erreicht!");
}
}
function wait(ms: number): Promise<void> {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
function waitFor(stateID: string, value: any): Promise<void> {
return new Promise(resolve => {
const handler = (obj: iobJS.ChangedStateObject) => {
if (obj.newState.val === value) {
unsubscribe(handler);
resolve();
}
}
subscribe(stateID, handler);
})
}
function setStateAsync(id: string, state: any): Promise<void> {
return new Promise(res => {
setState(id, state, () => res());
});
}
// === binary map parser from rand256/valetudo
const Tools = {
DIMENSION_PIXELS: 1024,
DIMENSION_MM: 50 * 1024
};
interface Pixels {
floor: number[],
obstacle: number[],
segments: number[],
}
interface ParsedMapData {
image?: any;
charger?: any;
robot?: any;
goto_target?: any;
currently_cleaned_zones?: any;
forbidden_zones?: any;
virtual_walls?: any;
currently_cleaned_blocks?: any;
}
const RRMapParser = {
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,
"DIGEST": 1024
},
PARSEBLOCK: function parseBlock(buf, offset, result?) {
result = result || {};
if (buf.length <= offset) {
return result;
}
let g3offset = 0;
var type = buf.readUInt16LE(0x00 + offset),
hlength = buf.readUInt16LE(0x02 + offset),
length = buf.readUInt32LE(0x04 + offset);
switch (type) {
case RRMapParser.TYPES.ROBOT_POSITION:
case RRMapParser.TYPES.CHARGER_LOCATION:
result[type] = {
position: [
buf.readUInt16LE(0x08 + offset),
buf.readUInt16LE(0x0c + offset)
],
angle: length >= 12 ? buf.readInt32LE(0x10 + offset) : 0 // gen3+
};
break;
case RRMapParser.TYPES.IMAGE:
if (hlength > 24) { // gen3+
g3offset = 4;
}
const parameters = {
segments: {
count: g3offset ? buf.readInt32LE(0x08 + offset) : 0,
id: [],
},
position: {
top: buf.readInt32LE(0x08 + g3offset + offset),
left: buf.readInt32LE(0x0c + g3offset + offset)
},
dimensions: {
height: buf.readInt32LE(0x10 + g3offset + offset),
width: buf.readInt32LE(0x14 + g3offset + offset)
},
pixels: {} as Pixels
};
parameters.position.top = Tools.DIMENSION_PIXELS - parameters.position.top - parameters.dimensions.height;
if (parameters.dimensions.height > 0 && parameters.dimensions.width > 0) {
parameters.pixels = {
floor: [],
obstacle: [],
segments: [],
};
for (let s, i = 0; i < length; i++) {
switch (buf.readUInt8(0x18 + g3offset + offset + i) & 0x07) {
case 0:
break;
case 1:
parameters.pixels.obstacle.push(i);
break;
default:
parameters.pixels.floor.push(i);
s = (buf.readUInt8(0x18 + g3offset + offset + i) & 248) >> 3;
if (s !== 0) {
if (!parameters.segments.id.includes(s)) parameters.segments.id.push(s);
parameters.pixels.segments.push(i | (s << 21));
}
break;
}
}
}
result[type] = parameters;
break;
case RRMapParser.TYPES.PATH:
case RRMapParser.TYPES.GOTO_PATH:
case RRMapParser.TYPES.GOTO_PREDICTED_PATH:
const points = [];
for (let i = 0; i < length; i = i + 4) {
//to draw these coordinates onto the map pixels, they have to be divided by 50
points.push([
buf.readUInt16LE(0x14 + offset + i),
buf.readUInt16LE(0x14 + offset + i + 2)
]);
}
result[type] = {
//point_count: buf.readUInt32LE(0x08 + offset),
//point_size: buf.readUInt32LE(0x0c + offset),
current_angle: buf.readUInt32LE(0x10 + offset), //This is always 0. Roborock didn't bother
points: points
};
break;
case RRMapParser.TYPES.GOTO_TARGET:
result[type] = {
position: [
buf.readUInt16LE(0x08 + offset),
buf.readUInt16LE(0x0a + offset)
]
};
break;
case RRMapParser.TYPES.CURRENTLY_CLEANED_ZONES:
const zoneCount = buf.readUInt32LE(0x08 + offset);
const zones = [];
if (zoneCount > 0) {
for (let i = 0; i < length; i = i + 8) {
zones.push([
buf.readUInt16LE(0x0c + offset + i),
buf.readUInt16LE(0x0c + offset + i + 2),
buf.readUInt16LE(0x0c + offset + i + 4),
buf.readUInt16LE(0x0c + offset + i + 6)
]);
}
result[type] = zones;
}
break;
case RRMapParser.TYPES.FORBIDDEN_ZONES:
const forbiddenZoneCount = buf.readUInt32LE(0x08 + offset);
const forbiddenZones = [];
if (forbiddenZoneCount > 0) {
for (let i = 0; i < length; i = i + 16) {
forbiddenZones.push([
buf.readUInt16LE(0x0c + offset + i),
buf.readUInt16LE(0x0c + offset + i + 2),
buf.readUInt16LE(0x0c + offset + i + 4),
buf.readUInt16LE(0x0c + offset + i + 6),
buf.readUInt16LE(0x0c + offset + i + 8),
buf.readUInt16LE(0x0c + offset + i + 10),
buf.readUInt16LE(0x0c + offset + i + 12),
buf.readUInt16LE(0x0c + offset + i + 14)
]);
}
result[type] = forbiddenZones;
}
break;
case RRMapParser.TYPES.VIRTUAL_WALLS:
const wallCount = buf.readUInt32LE(0x08 + offset);
const walls = [];
if (wallCount > 0) {
for (let i = 0; i < length; i = i + 8) {
walls.push([
buf.readUInt16LE(0x0c + offset + i),
buf.readUInt16LE(0x0c + offset + i + 2),
buf.readUInt16LE(0x0c + offset + i + 4),
buf.readUInt16LE(0x0c + offset + i + 6)
]);
}
result[type] = walls
}
break;
case RRMapParser.TYPES.CURRENTLY_CLEANED_BLOCKS:
const blockCount = buf.readUInt32LE(0x08 + offset);
const blocks = [];
if (blockCount > 0) {
for (let i = 0; i < length; i++) {
blocks.push(buf.readUInt8(0x0c + offset + i));
}
result[type] = blocks;
}
break;
}
return parseBlock(buf, offset + length + hlength, result);
},
/**
*
* @param mapBuf {Buffer} Should contain map in RRMap Format
* @return {object}
*/
PARSE: function parse(mapBuf) {
if (mapBuf[0x00] === 0x72 && mapBuf[0x01] === 0x72) {// rr
const parsedMapData = {
header_length: mapBuf.readUInt16LE(0x02),
data_length: mapBuf.readUInt16LE(0x04),
version: {
major: mapBuf.readUInt16LE(0x08),
minor: mapBuf.readUInt16LE(0x0A)
},
map_index: mapBuf.readUInt16LE(0x0C),
map_sequence: mapBuf.readUInt16LE(0x10)
};
return parsedMapData;
} else {
return {};
}
},
PARSEDATA: function parseData(mapBuf) {
if (!this.PARSE(mapBuf).map_index) {
return null;
}
const blocks = RRMapParser.PARSEBLOCK(mapBuf, 0x14);
const parsedMapData: ParsedMapData = {};
if (blocks[RRMapParser.TYPES.IMAGE]) { //We need the image to flip everything else correctly
parsedMapData.image = blocks[RRMapParser.TYPES.IMAGE];
[
{
type: RRMapParser.TYPES.PATH,
path: "path"
},
{
type: RRMapParser.TYPES.GOTO_PREDICTED_PATH,
path: "goto_predicted_path"
},
].forEach(item => {
if (blocks[item.type]) {
parsedMapData[item.path] = blocks[item.type];
parsedMapData[item.path].points = parsedMapData[item.path].points.map(point => {
point[1] = Tools.DIMENSION_MM - point[1];
return point;
});
if (parsedMapData[item.path].points.length >= 2) {
parsedMapData[item.path].current_angle =
Math.atan2(
parsedMapData[item.path].points[parsedMapData[item.path].points.length - 1][1] -
parsedMapData[item.path].points[parsedMapData[item.path].points.length - 2][1],
parsedMapData[item.path].points[parsedMapData[item.path].points.length - 1][0] -
parsedMapData[item.path].points[parsedMapData[item.path].points.length - 2][0]
) * 180 / Math.PI;
}
}
});
if (blocks[RRMapParser.TYPES.CHARGER_LOCATION]) {
parsedMapData.charger = blocks[RRMapParser.TYPES.CHARGER_LOCATION].position;
parsedMapData.charger[1] = Tools.DIMENSION_MM - parsedMapData.charger[1];
}
if (blocks[RRMapParser.TYPES.ROBOT_POSITION]) {
parsedMapData.robot = blocks[RRMapParser.TYPES.ROBOT_POSITION].position;
parsedMapData.robot[1] = Tools.DIMENSION_MM - parsedMapData.robot[1];
}
if (blocks[RRMapParser.TYPES.GOTO_TARGET]) {
parsedMapData.goto_target = blocks[RRMapParser.TYPES.GOTO_TARGET].position;
parsedMapData.goto_target[1] = Tools.DIMENSION_MM - parsedMapData.goto_target[1];
}
if (blocks[RRMapParser.TYPES.CURRENTLY_CLEANED_ZONES]) {
parsedMapData.currently_cleaned_zones = blocks[RRMapParser.TYPES.CURRENTLY_CLEANED_ZONES];
parsedMapData.currently_cleaned_zones = parsedMapData.currently_cleaned_zones.map(zone => {
zone[1] = Tools.DIMENSION_MM - zone[1];
zone[3] = Tools.DIMENSION_MM - zone[3];
return zone;
});
}
if (blocks[RRMapParser.TYPES.FORBIDDEN_ZONES]) {
parsedMapData.forbidden_zones = blocks[RRMapParser.TYPES.FORBIDDEN_ZONES];
parsedMapData.forbidden_zones = parsedMapData.forbidden_zones.map(zone => {
zone[1] = Tools.DIMENSION_MM - zone[1];
zone[3] = Tools.DIMENSION_MM - zone[3];
zone[5] = Tools.DIMENSION_MM - zone[5];
zone[7] = Tools.DIMENSION_MM - zone[7];
return zone;
})
}
if (blocks[RRMapParser.TYPES.VIRTUAL_WALLS]) {
parsedMapData.virtual_walls = blocks[RRMapParser.TYPES.VIRTUAL_WALLS];
parsedMapData.virtual_walls = parsedMapData.virtual_walls.map(wall => {
wall[1] = Tools.DIMENSION_MM - wall[1];
wall[3] = Tools.DIMENSION_MM - wall[3];
return wall;
});
}
if (blocks[RRMapParser.TYPES.CURRENTLY_CLEANED_BLOCKS]) {
parsedMapData.currently_cleaned_blocks = blocks[RRMapParser.TYPES.CURRENTLY_CLEANED_BLOCKS];
}
}
return parsedMapData;
},
};