Skip to content

Commit

Permalink
feat(vendor.dreame): ObstacleImagesCapability
Browse files Browse the repository at this point in the history
  • Loading branch information
Hypfer committed Sep 5, 2024
1 parent 30b5fc8 commit 67ee71c
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 8 deletions.
10 changes: 10 additions & 0 deletions backend/lib/robots/dreame/DreameL10SProUltraHeatValetudoRobot.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const Logger = require("../../Logger");
const MiioValetudoRobot = require("../MiioValetudoRobot");
const QuirksCapability = require("../../core/capabilities/QuirksCapability");
const ValetudoSelectionPreset = require("../../entities/core/ValetudoSelectionPreset");
const {IMAGE_FILE_FORMAT} = require("../../utils/const");

const stateAttrs = entities.state.attributes;

Expand Down Expand Up @@ -131,6 +132,15 @@ class DreameL10SProUltraHeatValetudoRobot extends DreameGen4ValetudoRobot {
liftSupported: true
}));

this.registerCapability(new capabilities.DreameObstacleImagesCapability({
robot: this,
fileFormat: IMAGE_FILE_FORMAT.JPG,
dimensions: {
width: 672,
height: 504
}
}));


[
capabilities.DreameCarpetModeControlCapability,
Expand Down
10 changes: 10 additions & 0 deletions backend/lib/robots/dreame/DreameL10SUltraValetudoRobot.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const fs = require("fs");
const MiioValetudoRobot = require("../MiioValetudoRobot");
const QuirksCapability = require("../../core/capabilities/QuirksCapability");
const ValetudoSelectionPreset = require("../../entities/core/ValetudoSelectionPreset");
const {IMAGE_FILE_FORMAT} = require("../../utils/const");

const stateAttrs = entities.state.attributes;

Expand Down Expand Up @@ -137,6 +138,15 @@ class DreameL10SUltraValetudoRobot extends DreameGen2LidarValetudoRobot {
liftSupported: true
}));

this.registerCapability(new capabilities.DreameObstacleImagesCapability({
robot: this,
fileFormat: IMAGE_FILE_FORMAT.JPG,
dimensions: {
width: 672,
height: 504
}
}));


[
capabilities.DreameCarpetModeControlCapability,
Expand Down
17 changes: 11 additions & 6 deletions backend/lib/robots/dreame/DreameMapParser.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const Logger = require("../../Logger");
const mapEntities = require("../../entities/map");
const uuid = require("uuid");
const zlib = require("zlib");

/**
Expand Down Expand Up @@ -32,11 +33,7 @@ class DreameMapParser {
const parsedHeader = DreameMapParser.PARSE_HEADER(buf.subarray(0, HEADER_SIZE));


/**
* Since P-Frame parsing is much harder than I-Frame parsing, we're skipping them for now
*
* If someone some day feels insanely motivated, feel free to add P-Frame support.
*/
// Since P-Frame parsing is much harder than I-Frame parsing, we're skipping them
if (parsedHeader.frame_type !== FRAME_TYPES.I) {
return null;
}
Expand Down Expand Up @@ -272,6 +269,7 @@ class DreameMapParser {
);
const type = OBSTACLE_TYPES[obstacle[2]] ?? `Unknown ID ${obstacle[2]}`;
const confidence = `${Math.round(parseFloat(obstacle[3])*100)}%`;
const image = obstacle[5] !== undefined ? obstacle[5] : undefined;

entities.push(new mapEntities.PointMapEntity({
points: [
Expand All @@ -280,7 +278,12 @@ class DreameMapParser {
],
type: mapEntities.PointMapEntity.TYPE.OBSTACLE,
metaData: {
label: `${type} (${confidence})`
label: `${type} (${confidence})`,
id: uuid.v5(
`${obstacle[2]}_${obstacle[0]}_${obstacle[1]}`,
OBSTACLE_ID_NAMESPACE
),
image: image
}
}));
});
Expand Down Expand Up @@ -701,4 +704,6 @@ DreameMapParser.CONVERT_ANGLE_TO_VALETUDO = function(angle) {
return ((angle < 180 ? 180 - angle : 360 - angle + 180) + 270) % 360;
};

const OBSTACLE_ID_NAMESPACE = "f90e13dc-3728-4267-bd90-43caa3f460e5";

module.exports = DreameMapParser;
5 changes: 4 additions & 1 deletion backend/lib/robots/dreame/DreameUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ class DreameUtils {
static DESERIALIZE_AI_SETTINGS(input) {
return {
obstacleDetection: !!(input & 0b00000010),
petObstacleDetection: !!(input & 0b00010000)
obstacleImages: !!(input & 0b00000100),
petObstacleDetection: !!(input & 0b00010000),
};
}

Expand All @@ -98,6 +99,7 @@ class DreameUtils {
let serializedValue = 0;

serializedValue |= input.obstacleDetection ? 0b00000010 : 0;
serializedValue |= input.obstacleImages ? 0b00000100 : 0;
serializedValue |= input.petObstacleDetection ? 0b00010000 : 0;

return serializedValue;
Expand All @@ -108,6 +110,7 @@ class DreameUtils {
* @typedef {object} AI_SETTINGS
* @property {boolean} obstacleDetection
* @property {boolean} petObstacleDetection
* @property {boolean} obstacleImages
*/

/**
Expand Down
10 changes: 10 additions & 0 deletions backend/lib/robots/dreame/DreameX10PlusValetudoRobot.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const fs = require("fs");
const MiioValetudoRobot = require("../MiioValetudoRobot");
const QuirksCapability = require("../../core/capabilities/QuirksCapability");
const ValetudoSelectionPreset = require("../../entities/core/ValetudoSelectionPreset");
const {IMAGE_FILE_FORMAT} = require("../../utils/const");

const stateAttrs = entities.state.attributes;

Expand Down Expand Up @@ -129,6 +130,15 @@ class DreameX10PlusValetudoRobot extends DreameGen2LidarValetudoRobot {
liftSupported: true
}));

this.registerCapability(new capabilities.DreameObstacleImagesCapability({
robot: this,
fileFormat: IMAGE_FILE_FORMAT.JPG,
dimensions: {
width: 672,
height: 504
}
}));


[
capabilities.DreameCarpetModeControlCapability,
Expand Down
10 changes: 10 additions & 0 deletions backend/lib/robots/dreame/DreameX40UltraValetudoRobot.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const Logger = require("../../Logger");
const MiioValetudoRobot = require("../MiioValetudoRobot");
const QuirksCapability = require("../../core/capabilities/QuirksCapability");
const ValetudoSelectionPreset = require("../../entities/core/ValetudoSelectionPreset");
const {IMAGE_FILE_FORMAT} = require("../../utils/const");

const stateAttrs = entities.state.attributes;

Expand Down Expand Up @@ -133,6 +134,15 @@ class DreameX40UltraValetudoRobot extends DreameGen4ValetudoRobot {
detachSupported: true
}));

this.registerCapability(new capabilities.DreameObstacleImagesCapability({
robot: this,
fileFormat: IMAGE_FILE_FORMAT.JPG,
dimensions: {
width: 672,
height: 504
}
}));


[
capabilities.DreameCarpetModeControlCapability,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const DreameMiotHelper = require("../DreameMiotHelper");
const DreameMiotServices = require("../DreameMiotServices");
const DreameUtils = require("../DreameUtils");
const fs = require("fs");
const Logger = require("../../../Logger");
const ObstacleImagesCapability = require("../../../core/capabilities/ObstacleImagesCapability");


/**
* @extends ObstacleImagesCapability<import("../DreameValetudoRobot")>
*/
class DreameObstacleImagesCapability extends ObstacleImagesCapability {
constructor(options) {
super(options);

this.siid = DreameMiotServices["GEN2"].VACUUM_2.SIID;
this.piid = DreameMiotServices["GEN2"].VACUUM_2.PROPERTIES.AI_CAMERA_SETTINGS.PIID;

this.helper = new DreameMiotHelper({robot: this.robot});
}

/**
* @returns {Promise<boolean>}
*/
async isEnabled() {
const res = await this.helper.readProperty(this.siid, this.piid);
const deserializedRes = DreameUtils.DESERIALIZE_AI_SETTINGS(res);

return deserializedRes.obstacleImages;
}

/**
* @returns {Promise<void>}
*/
async enable() {
const res = await this.helper.readProperty(this.siid, this.piid);
const deserializedRes = DreameUtils.DESERIALIZE_AI_SETTINGS(res);

deserializedRes.obstacleImages = true;

await this.helper.writeProperty(
this.siid,
this.piid,
DreameUtils.SERIALIZE_AI_SETTINGS(deserializedRes)
);
}

/**
* @returns {Promise<void>}
*/
async disable() {
const res = await this.helper.readProperty(this.siid, this.piid);
const deserializedRes = DreameUtils.DESERIALIZE_AI_SETTINGS(res);

deserializedRes.obstacleImages = false;

await this.helper.writeProperty(
this.siid,
this.piid,
DreameUtils.SERIALIZE_AI_SETTINGS(deserializedRes)
);
}

/*
* @param {string} image
* @returns {Promise<import('stream').Readable|null>}
*/
async getStreamForImage(image) {
if (!/^\/data\/record\/\d+\.jpg$/.test(image)) {
/*
Attack scenario:
someone somehow uploads a specially crafted map file containing a path for an obstacle image
that points somewhere that is no obstacle image
*/
Logger.warn("Unexpected obstacle image path.");

return null;
}

try {
return fs.createReadStream(image, {
highWaterMark: 32 * 1024,
autoClose: true
});
} catch (err) {
if (err.code === "ENOENT") {
return null;
} else {
throw new Error(`Unexpected error while trying to read obstacle image: ${err.message}`);
}
}
}
}

module.exports = DreameObstacleImagesCapability;
1 change: 1 addition & 0 deletions backend/lib/robots/dreame/capabilities/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ module.exports = {
DreameMopDockDryManualTriggerCapability: require("./DreameMopDockDryManualTriggerCapability"),
DreameMopDockWaterUsageControlCapability: require("./DreameMopDockWaterUsageControlCapability"),
DreameMopMappingPassCapability: require("./DreameMopMappingPassCapability"),
DreameObstacleImagesCapability: require("./DreameObstacleImagesCapability"),
DreameOperationModeControlCapability: require("./DreameOperationModeControlCapability"),
DreamePendingMapChangeHandlingCapability: require("./DreamePendingMapChangeHandlingCapability"),
DreamePersistentMapControlCapability: require("./DreamePersistentMapControlCapability"),
Expand Down
18 changes: 18 additions & 0 deletions backend/test/lib/robots/dreame/DreameUtils_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,23 @@ describe("DreameUtils", function () {

actual.should.deepEqual({
obstacleDetection: true,
obstacleImages: true,
petObstacleDetection: true
});

const actual2 = DreameUtils.DESERIALIZE_AI_SETTINGS(15);

actual2.should.deepEqual({
obstacleDetection: true,
obstacleImages: true,
petObstacleDetection: false
});

const actual3 = DreameUtils.DESERIALIZE_AI_SETTINGS(4);

actual3.should.deepEqual({
obstacleDetection: false,
obstacleImages: true,
petObstacleDetection: false
});
});
Expand All @@ -98,6 +108,14 @@ describe("DreameUtils", function () {
});

actual2.should.equal(2);

const actual3 = DreameUtils.SERIALIZE_AI_SETTINGS({
obstacleDetection: false,
obstacleImages: true,
petObstacleDetection: false
});

actual3.should.equal(4);
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -6256,7 +6256,9 @@
{
"__class": "PointMapEntity",
"metaData": {
"label": "Pedestal (89%)"
"label": "Pedestal (89%)",
"id": "33911310-ff43-529c-862b-4765035ecd34",
"image": "/data/record/1.jpg"
},
"points": [
3325,
Expand Down

0 comments on commit 67ee71c

Please sign in to comment.