Skip to content

Commit

Permalink
feat(vendor.dreame): Virtual Restrictions
Browse files Browse the repository at this point in the history
  • Loading branch information
Hypfer committed Jan 29, 2021
1 parent bc6e243 commit 3f1f494
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 22 deletions.
110 changes: 104 additions & 6 deletions lib/DreameMapParser.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const zlib = require("zlib");
const Map = require("./entities/map");
const Logger = require("./Logger");

Expand All @@ -15,9 +16,10 @@ class DreameMapParser {
* :(
*
* @param {Buffer} buf
* @param {MapDataType} [type]
* @returns {null|import("./entities/map/ValetudoMap")}
*/
static PARSE(buf) {
static PARSE(buf, type) {
//Maps are always at least 27 bytes in size
if (!buf || buf.length < HEADER_SIZE) {
return null;
Expand All @@ -35,7 +37,7 @@ class DreameMapParser {
return null;
}

let layers;
let layers = [];
const entities = [
new Map.PointMapEntity({
points: [//Intentionally swapped x/y
Expand Down Expand Up @@ -81,7 +83,13 @@ class DreameMapParser {
}

if (additionalData.da2 && Array.isArray(additionalData.da2.areas)) {
entities.push(...DreameMapParser.PARSE_ACTIVE_ZONES(parsedHeader, additionalData.da2.areas));
entities.push(
...DreameMapParser.PARSE_AREAS(
parsedHeader,
additionalData.da2.areas,
Map.PolygonMapEntity.TYPE.ACTIVE_ZONE
)
);
}

if (additionalData.sa && Array.isArray(additionalData.sa)) {
Expand All @@ -90,7 +98,60 @@ class DreameMapParser {
});
}

layers = DreameMapParser.PARSE_IMAGE(parsedHeader, activeSegmentIds, imageData);
if (additionalData.vw) {
if (Array.isArray(additionalData.vw.rect)) {
entities.push(
...DreameMapParser.PARSE_AREAS(
parsedHeader,
additionalData.vw.rect,
Map.PolygonMapEntity.TYPE.NO_GO_AREA
)
);
}

if (Array.isArray(additionalData.vw.mop)) {
entities.push(
...DreameMapParser.PARSE_AREAS(
parsedHeader,
additionalData.vw.mop,
Map.PolygonMapEntity.TYPE.NO_MOP_AREA
)
);
}

if (Array.isArray(additionalData.vw.line)) {
entities.push(
...DreameMapParser.PARSE_LINES(
parsedHeader,
additionalData.vw.line,
Map.LineMapEntity.TYPE.VIRTUAL_WALL
)
);
}
}

/**
* Contains saved map data such as virtual restrictions
*/
if (additionalData.rism) { //TODO: SM = Saved Map?
const rismResult = DreameMapParser.PARSE(DreameMapParser.PREPROCESS(additionalData.rism), MAP_DATA_TYPES.RISM);

if (rismResult instanceof Map.ValetudoMap) {
rismResult.entities.filter(e => {
return (e instanceof Map.LineMapEntity && e.type === Map.LineMapEntity.TYPE.VIRTUAL_WALL) ||
(e instanceof Map.PolygonMapEntity && e.type === Map.PolygonMapEntity.TYPE.NO_GO_AREA) ||
(e instanceof Map.PolygonMapEntity && e.type === Map.PolygonMapEntity.TYPE.NO_MOP_AREA);
}).forEach(e => {
entities.push(e);
});
}

//TODO: is there more?
}

if (type !== MAP_DATA_TYPES.RISM) { //TODO: This could be nicer
layers = DreameMapParser.PARSE_IMAGE(parsedHeader, activeSegmentIds, imageData);
}
} else {
//Just a header
return null;
Expand Down Expand Up @@ -298,10 +359,10 @@ class DreameMapParser {
return path;
}

static PARSE_ACTIVE_ZONES(parsedHeader, areas) {
static PARSE_AREAS(parsedHeader, areas, type) {
return areas.map(a => {
return new Map.PolygonMapEntity({
type: Map.PolygonMapEntity.TYPE.ACTIVE_ZONE,
type: type,
points: [
Math.round((a[1] + HALF_INT16)/10) - parsedHeader.top,
Math.round((a[0] + HALF_INT16)/10) - parsedHeader.left,
Expand All @@ -318,6 +379,33 @@ class DreameMapParser {
});
});
}

static PARSE_LINES(parsedHeader, lines, type) {
return lines.map(a => {
return new Map.LineMapEntity({
type: type,
points: [
Math.round((a[1] + HALF_INT16)/10) - parsedHeader.top,
Math.round((a[0] + HALF_INT16)/10) - parsedHeader.left,

Math.round((a[3] + HALF_INT16)/10) - parsedHeader.top,
Math.round((a[2] + HALF_INT16)/10) - parsedHeader.left,
]
});
});
}

/**
* Uploaded dreame Maps are actually base64 strings of zlib compressed data with two characters replaced
*
* @param {any} data
* @returns {Buffer}
*/
static PREPROCESS(data) {
const base64String = data.toString().replace(/_/g, "/").replace(/-/g, "+");

return zlib.inflateSync(Buffer.from(base64String, "base64"));
}
}

const PIXEL_TYPES = Object.freeze({
Expand All @@ -331,6 +419,16 @@ const FRAME_TYPES = Object.freeze({
P: 80
});

/**
* @typedef {string} MapDataType
* @enum {string}
*
*/
const MAP_DATA_TYPES = Object.freeze({
REGULAR: "regular",
RISM: "rism"
});

const HALF_INT16 = 32768;
const HALF_INT16_UPPER_HALF = 32767;
const HEADER_SIZE = 27;
Expand Down
7 changes: 7 additions & 0 deletions lib/robots/dreame/DreameD9ValetudoRobot.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ const MIOT_SERVICES = Object.freeze({
},
VIRTUAL_RESTRICTIONS: {
PIID: 4
},

ACTION_RESULT: {
PIID: 6
}
},
ACTIONS: {
Expand Down Expand Up @@ -195,6 +199,9 @@ class DreameD9ValetudoRobot extends DreameValetudoRobot {
miot_properties: {
virtualRestrictions: {
piid: MIOT_SERVICES.MAP.PROPERTIES.VIRTUAL_RESTRICTIONS.PIID
},
actionResult: {
piid: MIOT_SERVICES.MAP.PROPERTIES.ACTION_RESULT.PIID
}
}
}));
Expand Down
20 changes: 7 additions & 13 deletions lib/robots/dreame/DreameValetudoRobot.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const zlib = require("zlib");
const capabilities = require("./capabilities");

const DreameMapParser = require("../../DreameMapParser");
Expand Down Expand Up @@ -134,19 +133,14 @@ class DreameValetudoRobot extends MiioValetudoRobot {
* @returns {Promise<Buffer>}
*/
preprocessMap(data) {
const base64String = data.toString().replace(/_/g, "/").replace(/-/g, "+");


return new Promise((resolve, reject) => {
zlib.inflate(
Buffer.from(base64String, "base64"),
(err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
try {
const preprocessedData = DreameMapParser.PREPROCESS(data);

resolve(preprocessedData);
} catch (e) {
reject(e);
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class DreameCombinedVirtualRestrictionsCapability extends CombinedVirtualRestric
* @param {object} options.miot_properties
* @param {object} options.miot_properties.virtualRestrictions
* @param {number} options.miot_properties.virtualRestrictions.piid
*
* @param {object} options.miot_properties.actionResult
* @param {number} options.miot_properties.actionResult.piid
*/
constructor(options) {
super(options);
Expand Down Expand Up @@ -50,15 +53,27 @@ class DreameCombinedVirtualRestrictionsCapability extends CombinedVirtualRestric
* also an entity
* Planning ahead was actually worth it :)
*/
throw new Error("Cannot start zoned cleanup due to missing map data");
throw new Error("Cannot save virtual restrictions due to missing map data");
}

const offsets = this.robot.state.map.metaData.dreame.offsets;
const dreamePayload = {
rect: [],
line: [],
rect: []
mop: []
};

/**
* The payload can also contain a key named "temp" with an empty object as its value
* That can be used to draw virtual restrictions if the map is only temporary.
*
* This is a useful feature for the initial mapping process, since you're able to pause the robot
* and draw a virtual restriction instead of having to remove all obstacles once for the initial
* mapping run
*
* We won't use that so we'll leave this out.
*/

if (virtualRestrictions.virtualWalls.length >= 10) {
throw new Error("Too many virtual Walls to save");
}
Expand Down Expand Up @@ -97,7 +112,23 @@ class DreameCombinedVirtualRestrictionsCapability extends CombinedVirtualRestric
}
]
}
).finally(() => {
).then(res => {
if (
res && res.siid === this.miot_actions.map_edit.siid &&
res.aiid === this.miot_actions.map_edit.aiid &&
Array.isArray(res.out) && res.out.length === 1 &&
res.out[0].piid === this.miot_properties.actionResult.piid
) {
switch (res.out[0].value) {
case 0:
return;
case 10:
throw new Error("Cannot save temporary virtual restrictions. A persistent map exists.");
default:
throw new Error("Got error " + res.out[0].value + " while saving virtual restrictions.");
}
}
}).finally(() => {
this.robot.pollMap();
});
}
Expand Down

0 comments on commit 3f1f494

Please sign in to comment.