Skip to content

Commit

Permalink
feat(vendor.viomi): Refactoring, fixes and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Hypfer committed Nov 11, 2022
1 parent ab7ec30 commit 7882db4
Show file tree
Hide file tree
Showing 9 changed files with 2,537 additions and 49 deletions.
74 changes: 49 additions & 25 deletions backend/lib/robots/viomi/ViomiMapParser.js
@@ -1,5 +1,6 @@
const Logger = require("../../Logger");
const Map = require("../../entities/map");
const zlib = require("zlib");

/** @typedef {Array<number>} Pose */

Expand Down Expand Up @@ -220,7 +221,7 @@ class ViomiMapParser {
this.take(this.buf.length - this.offset, "trailing");

// TODO: one of them is just the room outline, not actual past navigation logic
return this.convertToValetudoMap({
const valetudoMap = this.convertToValetudoMap({
image: this.img,
zones: this.rooms,
//TODO: at least according to all my sample files, this.points is never the path
Expand All @@ -236,6 +237,14 @@ class ViomiMapParser {
no_go_area: this.no_go_area,
clean_area: this.clean_area
});


if (valetudoMap.layers.length > 0) {
return valetudoMap;
} else {
// Occasionally, we might receive an empty map with all pixels set to 0
return null;
}
}

/**
Expand All @@ -246,15 +255,11 @@ class ViomiMapParser {
* @return {number}
*/
viomiToValetudoAngle(angle) {
let result = (-180 - (angle * 180 / Math.PI)) % 360;
while (result < 0) {
result += 360;
}
return result;
return (angle + 180) % 360;
}

/**
* This is a temporary conversion function which should at some point be replaced with a complete rewrite
* This is a temporary conversion function, which should at some point be replaced with a complete rewrite
* of the viomi parser.
*
* For now however, this shall suffice
Expand All @@ -280,18 +285,23 @@ class ViomiMapParser {
// The charger angle is usually always provided.
// The robot angle may be 0, usually when the robot is docked.
let chargerAngle = mapContents.charger_angle !== undefined ? this.viomiToValetudoAngle(mapContents.charger_angle) : 0;
let robotAngle = (mapContents.robot_angle !== undefined && mapContents.robot_angle !== 0) ? this.viomiToValetudoAngle(mapContents.robot_angle) : chargerAngle;
Logger.trace("Raw robot angle", mapContents.robot_angle, mapContents.robot_angle * 180 / Math.PI, "calculated", robotAngle);
let robotAngle = mapContents.robot_angle !== undefined ? this.viomiToValetudoAngle(mapContents.robot_angle) : chargerAngle;

if (mapContents.image) {
layers.push(new Map.MapLayer({
pixels: mapContents.image.pixels.floor.sort(Map.MapLayer.COORDINATE_TUPLE_SORT).flat(),
type: Map.MapLayer.TYPE.FLOOR
}));
layers.push(new Map.MapLayer({
pixels: mapContents.image.pixels.obstacle_strong.sort(Map.MapLayer.COORDINATE_TUPLE_SORT).flat(),
type: Map.MapLayer.TYPE.WALL
}));
if (mapContents.image.pixels.floor.length > 0) {
layers.push(new Map.MapLayer({
pixels: mapContents.image.pixels.floor.sort(Map.MapLayer.COORDINATE_TUPLE_SORT).flat(),
type: Map.MapLayer.TYPE.FLOOR
}));
}

if (mapContents.image.pixels.wall.length > 0) {
layers.push(new Map.MapLayer({
pixels: mapContents.image.pixels.wall.sort(Map.MapLayer.COORDINATE_TUPLE_SORT).flat(),
type: Map.MapLayer.TYPE.WALL
}));
}


if (mapContents.image.pixels.rooms && mapContents.zones) {
Object.keys(mapContents.image.pixels.rooms).forEach(segmentId => {
Expand Down Expand Up @@ -327,7 +337,7 @@ class ViomiMapParser {

mapContents.path.points[mapContents.path.points.length - 2] -
mapContents.path.points[mapContents.path.points.length - 4]
) * 180 / Math.PI) + 270) % 360; //TODO: No idea why
) * 180 / Math.PI) + 90) % 360; //TODO: No idea why
}
}

Expand Down Expand Up @@ -527,7 +537,7 @@ class ViomiMapParser {
const width = this.mapHead.readUInt32LE(16);
let pixels = {
floor: [],
obstacle_strong: [],
wall: [],
rooms: {}
};
if (height > 0 && width > 0) {
Expand All @@ -541,18 +551,20 @@ class ViomiMapParser {
// non-floor, do nothing
break;
case 255:
pixels.obstacle_strong.push([coords[0], coords[1]]);
pixels.wall.push([coords[0], coords[1]]);
break;
case 1: // non-room
pixels.floor.push([coords[0], coords[1]]);
break;
default:
if (!Array.isArray(pixels.rooms[val])) {
pixels.rooms[val] = [];
default: {
const segmentId = val >= 60 ? val - 50 : val; //TODO: this can't be right but it works?
if (!Array.isArray(pixels.rooms[segmentId])) {
pixels.rooms[segmentId] = [];
}

pixels.rooms[val].push([coords[0], coords[1]]);
pixels.floor.push([coords[0], coords[1]]);
pixels.rooms[segmentId].push([coords[0], coords[1]]);
}

}
}
}
Expand Down Expand Up @@ -606,6 +618,18 @@ class ViomiMapParser {
y: ViomiMapParser.MAX_MAP_HEIGHT - ViomiMapParser.convertFloat(y)
};
}

/**
* @param {Buffer|string} data
* @returns {Promise<Buffer>}
*/
static async PREPROCESS(data) {
return new Promise((resolve, reject) => {
zlib.inflate(data, (err, result) => {
return err ? reject(err) : resolve(result);
});
});
}
}

/**
Expand Down
44 changes: 44 additions & 0 deletions backend/lib/robots/viomi/ViomiV6ValetudoRobot.js
@@ -0,0 +1,44 @@
const capabilities = require("./capabilities");
const MiioValetudoRobot = require("../MiioValetudoRobot");
const QuirksCapability = require("../../core/capabilities/QuirksCapability");
const ViomiQuirkFactory = require("./ViomiQuirkFactory");
const ViomiValetudoRobot = require("./ViomiValetudoRobot");


class ViomiV6ValetudoRobot extends ViomiValetudoRobot {
/**
* @param {object} options
* @param {import("../../Configuration")} options.config
* @param {import("../../ValetudoEventStore")} options.valetudoEventStore
* @param {object} [options.fanSpeeds]
* @param {object} [options.waterGrades]
*/
constructor(options) {
super(options);

this.registerCapability(new capabilities.ViomiVoicePackManagementCapability({
robot: this
}));

const quirkFactory = new ViomiQuirkFactory({
robot: this
});
this.registerCapability(new QuirksCapability({
robot: this,
quirks: [
quirkFactory.getQuirk(ViomiQuirkFactory.KNOWN_QUIRKS.BUTTON_LEDS)
]
}));
}

getModelName() {
return "V6";
}

static IMPLEMENTATION_AUTO_DETECTION_HANDLER() {
const deviceConf = MiioValetudoRobot.READ_DEVICE_CONF(ViomiValetudoRobot.DEVICE_CONF_PATH);
return !!(deviceConf && deviceConf.model === "viomi.vacuum.v6");
}
}

module.exports = ViomiV6ValetudoRobot;
3 changes: 1 addition & 2 deletions backend/lib/robots/viomi/ViomiV7ValetudoRobot.js
Expand Up @@ -14,7 +14,6 @@ class ViomiV7ValetudoRobot extends ViomiValetudoRobot {
*/
constructor(options) {
super(options);
// TODO: register model-specific capabilities

const quirkFactory = new ViomiQuirkFactory({
robot: this
Expand All @@ -33,7 +32,7 @@ class ViomiV7ValetudoRobot extends ViomiValetudoRobot {

static IMPLEMENTATION_AUTO_DETECTION_HANDLER() {
const deviceConf = MiioValetudoRobot.READ_DEVICE_CONF(ViomiValetudoRobot.DEVICE_CONF_PATH);
return !!(deviceConf && ["viomi.vacuum.v6", "viomi.vacuum.v7", "viomi.vacuum.v8", "viomi.vacuum.v9"].includes(deviceConf.model));
return !!(deviceConf && deviceConf.model === "viomi.vacuum.v7");
}
}

Expand Down
44 changes: 44 additions & 0 deletions backend/lib/robots/viomi/ViomiV8ValetudoRobot.js
@@ -0,0 +1,44 @@
const capabilities = require("./capabilities");
const MiioValetudoRobot = require("../MiioValetudoRobot");
const QuirksCapability = require("../../core/capabilities/QuirksCapability");
const ViomiQuirkFactory = require("./ViomiQuirkFactory");
const ViomiValetudoRobot = require("./ViomiValetudoRobot");


class ViomiV8ValetudoRobot extends ViomiValetudoRobot {
/**
* @param {object} options
* @param {import("../../Configuration")} options.config
* @param {import("../../ValetudoEventStore")} options.valetudoEventStore
* @param {object} [options.fanSpeeds]
* @param {object} [options.waterGrades]
*/
constructor(options) {
super(options);

this.registerCapability(new capabilities.ViomiVoicePackManagementCapability({
robot: this
}));

const quirkFactory = new ViomiQuirkFactory({
robot: this
});
this.registerCapability(new QuirksCapability({
robot: this,
quirks: [
quirkFactory.getQuirk(ViomiQuirkFactory.KNOWN_QUIRKS.BUTTON_LEDS)
]
}));
}

getModelName() {
return "V8";
}

static IMPLEMENTATION_AUTO_DETECTION_HANDLER() {
const deviceConf = MiioValetudoRobot.READ_DEVICE_CONF(ViomiValetudoRobot.DEVICE_CONF_PATH);
return !!(deviceConf && ["viomi.vacuum.v8", "viomi.vacuum.v9"].includes(deviceConf.model));
}
}

module.exports = ViomiV8ValetudoRobot;
36 changes: 14 additions & 22 deletions backend/lib/robots/viomi/ViomiValetudoRobot.js
Expand Up @@ -96,10 +96,6 @@ class ViomiValetudoRobot extends MiioValetudoRobot {
robot: this
}));

this.registerCapability(new capabilities.ViomiVoicePackManagementCapability({
robot: this
}));

this.registerCapability(new capabilities.ViomiCarpetModeControlCapability({
robot: this,
carpetConfigFile: "/mnt/UDISK/config/new_user_perference.txt"
Expand Down Expand Up @@ -182,7 +178,7 @@ class ViomiValetudoRobot extends MiioValetudoRobot {
*/
sendCommand(method, args = [], options = {}) {
options = Object.assign({
timeout: 2000,
timeout: typeof options.timeout === "number" ? Math.max(3000, options.timeout) : 3000,
}, options);
return super.sendCommand(method, args, options);
}
Expand Down Expand Up @@ -212,6 +208,13 @@ class ViomiValetudoRobot extends MiioValetudoRobot {
return true;
}

if (msg.method === "props") {
if (msg.params?.ota_state !== undefined) {
this.sendCloud({id: msg.id, "result":"ok"});
return true;
}
}

return super.onIncomingCloudMessage(msg);
}

Expand Down Expand Up @@ -444,15 +447,6 @@ class ViomiValetudoRobot extends MiioValetudoRobot {
this.capabilities[capabilities.ViomiPersistentMapControlCapability.TYPE].persistentMapState = data["remember_map"] === 1;
}

// Adjust timezone if != UTC
if (data["timezone"] !== undefined && data["timezone"] !== 0) {
this.sendCommand("set_timezone", [0], {timeout: 12000}).then(_ => {
Logger.info("Viomi timezone adjusted to UTC");
}).catch(err => {
Logger.warn("Error while adjusting timezone to UTC");
});
}

this.emitStateAttributesUpdated();
}

Expand All @@ -461,22 +455,20 @@ class ViomiValetudoRobot extends MiioValetudoRobot {
}

preprocessMap(data) {
return new Promise((resolve, reject) => {
zlib.inflate(data, (err, result) => {
return err ? reject(err) : resolve(result);
});
});
return ViomiMapParser.PREPROCESS(data);
}

async parseMap(data) {
try {
// noinspection UnnecessaryLocalVariableJS
const map = new ViomiMapParser(data).parse();

this.state.map = map;
if (map !== null) {
this.state.map = map;
this.emitMapUpdated();
}

this.emitMapUpdated();
return this.state.map; //TODO
return this.state.map;
} catch (e) {
let i = 0;
let filename = "";
Expand Down
2 changes: 2 additions & 0 deletions backend/lib/robots/viomi/index.js
@@ -1,3 +1,5 @@
module.exports = {
"ViomiV6ValetudoRobot": require("./ViomiV6ValetudoRobot"),
"ViomiV7ValetudoRobot": require("./ViomiV7ValetudoRobot"),
"ViomiV8ValetudoRobot": require("./ViomiV8ValetudoRobot"),
};
34 changes: 34 additions & 0 deletions backend/test/lib/robots/viomi/ViomiMapParser_spec.js
@@ -0,0 +1,34 @@
const fs = require("fs").promises;
const path = require("path");
const should = require("should");

const ViomiMapParser = require("../../../../lib/robots/viomi/ViomiMapParser");

should.config.checkProtoEql = false;

describe("ViomiMapParser", function () {
it("Should pre-process & parse v7 fw 47 map with currently cleaned segments", async function() {
let data = await fs.readFile(path.join(__dirname, "/res/map/v7_47_cleaned_segment_ids.bin"));
let expected = JSON.parse(await fs.readFile(path.join(__dirname, "/res/map/v7_47_cleaned_segment_ids.json"), { encoding: "utf-8" }));
const parserInstance = new ViomiMapParser(await ViomiMapParser.PREPROCESS(data));
let actual = parserInstance.parse();

if (actual.metaData?.nonce) {
delete(actual.metaData.nonce);
}

actual.layers.length.should.equal(expected.layers.length, "layerCount");

actual.layers.forEach((layer, i) => {
actual.layers[i].should.deepEqual(expected.layers[i]);
});

actual.entities.length.should.equal(expected.entities.length, "entitiesCount");

actual.entities.forEach((layer, i) => {
actual.entities[i].should.deepEqual(expected.entities[i]);
});

actual.should.deepEqual(expected);
});
});
Binary file not shown.

0 comments on commit 7882db4

Please sign in to comment.