mirror of
https://github.com/Hypfer/Valetudo.git
synced 2025-10-26 11:27:27 +00:00
feat(vendor.midea): Obstacles galore
This commit is contained in:
parent
a963fa17ee
commit
17ebfae6a8
1
.gitignore
vendored
1
.gitignore
vendored
@ -15,6 +15,7 @@ build/
|
||||
|
||||
/backend/lib/res/valetudo.openapi.schema.json
|
||||
/backend/lib/res/build_metadata.json
|
||||
/backend/lib/robots/midea/generated/midea_protobufs.js
|
||||
|
||||
/docs/vendor
|
||||
/docs/_site
|
||||
|
||||
83
assets/protobuf/midea.proto
Normal file
83
assets/protobuf/midea.proto
Normal file
@ -0,0 +1,83 @@
|
||||
syntax = "proto2";
|
||||
|
||||
message GridIndex {
|
||||
optional int32 x = 1;
|
||||
optional int32 y = 2;
|
||||
}
|
||||
|
||||
message Point {
|
||||
optional float x = 1;
|
||||
optional float y = 2;
|
||||
optional float z = 3;
|
||||
}
|
||||
|
||||
message PointCloud {
|
||||
repeated Point points = 1;
|
||||
}
|
||||
|
||||
message AIRect {
|
||||
optional int32 x = 1;
|
||||
optional int32 y = 2;
|
||||
optional int32 width = 3;
|
||||
optional int32 height = 4;
|
||||
}
|
||||
|
||||
message AIImageInfo {
|
||||
optional int32 confidence = 1;
|
||||
optional string absolute_path = 2;
|
||||
optional AIRect rect = 3;
|
||||
optional float float_val_10 = 10;
|
||||
optional float float_val_11 = 11;
|
||||
}
|
||||
|
||||
message BoundingBox {
|
||||
optional GridIndex corner_1 = 1;
|
||||
optional GridIndex corner_2 = 2;
|
||||
optional GridIndex corner_3 = 3;
|
||||
optional GridIndex corner_4 = 4;
|
||||
}
|
||||
|
||||
message SemanticObject {
|
||||
optional int32 object_type = 1;
|
||||
optional int32 instance_id = 2; // e.g. the 5th shoe seen
|
||||
optional GridIndex center_point = 3;
|
||||
|
||||
// index 0: A serialized BoundingBox message.
|
||||
// index 1: Some kind of hash?
|
||||
repeated bytes field_4_data = 4;
|
||||
|
||||
optional int32 int32_val_5 = 5;
|
||||
optional int32 int32_val_6 = 6;
|
||||
optional int64 timestamp_us = 7;
|
||||
optional PointCloud point_cloud = 8;
|
||||
optional AIImageInfo ai_image_info = 9;
|
||||
optional float float_val_10 = 10;
|
||||
optional float float_val_11 = 11;
|
||||
optional int32 int32_val_12 = 12;
|
||||
optional int32 int32_val_13 = 13;
|
||||
optional int32 int32_val_14 = 14;
|
||||
optional int32 int32_val_15 = 15;
|
||||
}
|
||||
|
||||
message SemanticMapInfo {
|
||||
repeated SemanticObject objects = 1;
|
||||
optional uint32 uint32_val_2 = 2;
|
||||
optional uint32 uint32_val_3 = 3;
|
||||
optional string clean_record_id = 4;
|
||||
}
|
||||
|
||||
message TrackPoint {
|
||||
optional int64 int64_val_1 = 1;
|
||||
optional int32 int32_val_2 = 2;
|
||||
optional string name = 3;
|
||||
optional string desc = 4;
|
||||
optional int32 int32_val_5 = 5;
|
||||
optional int32 int32_val_6 = 6;
|
||||
|
||||
oneof data_oneof {
|
||||
int64 oneof_int64_7 = 7;
|
||||
int64 oneof_int64_8 = 8;
|
||||
float oneof_float_9 = 9;
|
||||
string oneof_string_10 = 10;
|
||||
}
|
||||
}
|
||||
1
backend/.eslintignore
Normal file
1
backend/.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
/lib/robots/midea/generated/midea_protobufs.js
|
||||
@ -254,6 +254,9 @@ class BEightParser {
|
||||
data.cross_bridge_switch = !!(generalSwitchBits2 & 0b00000100);
|
||||
data.camera_led_switch = !!(generalSwitchBits2 & 0b00001000);
|
||||
data.map_3d_switch = !!(generalSwitchBits2 & 0b00010000);
|
||||
|
||||
// Master toggle. When disabled, everything else will disable itself
|
||||
// This is set to true once the user accepts some ToS
|
||||
data.ai_recognition_switch = !!(generalSwitchBits2 & 0b00100000);
|
||||
|
||||
data.test_mode_type = payload[54];
|
||||
@ -297,9 +300,24 @@ class BEightParser {
|
||||
|
||||
if (payload.length >= 71) {
|
||||
const generalSwitchBits7 = payload[70];
|
||||
data.big_object_detect_switch = !!(generalSwitchBits7 & 0b00000001);
|
||||
|
||||
// possibly to know which room is which? What does the firmware do with it?
|
||||
data.furniture_identify_switch = !!(generalSwitchBits7 & 0b00000001);
|
||||
data.fall_detection_switch = !!(generalSwitchBits7 & 0b00000100);
|
||||
data.obstacle_image_upload_switch = !!(generalSwitchBits7 & 0b00001000);
|
||||
data.threshold_recognition_switch = !!(generalSwitchBits7 & 0b01000000);
|
||||
data.curtain_recognition_switch = !!(generalSwitchBits7 & 0b10000000);
|
||||
}
|
||||
|
||||
if (payload.length >= 74) {
|
||||
const generalSwitchBits8 = payload[73];
|
||||
|
||||
data.adb_switch = !!(generalSwitchBits8 & 0b00000001);
|
||||
data.station_v2_switch = !!(generalSwitchBits8 & 0b00000010);
|
||||
data.static_stain_recognition_switch = !!(generalSwitchBits8 & 0b00000100);
|
||||
}
|
||||
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@ -454,6 +454,24 @@ class MSmartDummycloud {
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
app.post("/v1/biz/file/device/uploadFileUrl", (req, res) => {
|
||||
Logger.trace("Received request for a new presigned file upload URL");
|
||||
|
||||
res.status(200).json({
|
||||
code: "0",
|
||||
msg: "OK",
|
||||
data: {
|
||||
url: `https://${req.hostname}/_valetudo/fileUpload?ts=${Date.now()}`
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.put("/_valetudo/fileUpload", (req, res) => {
|
||||
Logger.trace("Received file upload");
|
||||
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
|
||||
app.all("*", (req, res) => {
|
||||
if (this.onHttpRequest) {
|
||||
|
||||
@ -88,7 +88,14 @@ class MSmartStatusDTO extends MSmartDTO {
|
||||
* @param {boolean} [data.gap_deep_cleaning_switch]
|
||||
* @param {boolean} [data.furniture_legs_cleaning_switch]
|
||||
* @param {boolean} [data.edge_deep_vacuum_switch]
|
||||
* @param {boolean} [data.big_object_detect_switch]
|
||||
* @param {boolean} [data.furniture_identify_switch]
|
||||
* @param {boolean} [data.fall_detection_switch]
|
||||
* @param {boolean} [data.obstacle_image_upload_switch]
|
||||
* @param {boolean} [data.threshold_recognition_switch]
|
||||
* @param {boolean} [data.curtain_recognition_switch]
|
||||
* @param {boolean} [data.adb_switch]
|
||||
* @param {boolean} [data.station_v2_switch]
|
||||
* @param {boolean} [data.static_stain_recognition_switch]
|
||||
*/
|
||||
constructor(data) {
|
||||
super();
|
||||
@ -178,7 +185,14 @@ class MSmartStatusDTO extends MSmartDTO {
|
||||
this.gap_deep_cleaning_switch = data.gap_deep_cleaning_switch;
|
||||
this.furniture_legs_cleaning_switch = data.furniture_legs_cleaning_switch;
|
||||
this.edge_deep_vacuum_switch = data.edge_deep_vacuum_switch;
|
||||
this.big_object_detect_switch = data.big_object_detect_switch;
|
||||
this.furniture_identify_switch = data.furniture_identify_switch;
|
||||
this.fall_detection_switch = data.fall_detection_switch;
|
||||
this.obstacle_image_upload_switch = data.obstacle_image_upload_switch;
|
||||
this.threshold_recognition_switch = data.threshold_recognition_switch;
|
||||
this.curtain_recognition_switch = data.curtain_recognition_switch;
|
||||
this.adb_switch = data.adb_switch;
|
||||
this.station_v2_switch = data.station_v2_switch;
|
||||
this.static_stain_recognition_switch = data.static_stain_recognition_switch;
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
@ -53,7 +53,45 @@ tkciowVpUEiXK0tg/t8EgHj+sW9vUqX6ovKDx5Hy2nczd2CruRxHUieSQKX+zajv
|
||||
-----END CERTIFICATE-----
|
||||
`;
|
||||
|
||||
const AI_OBSTACLE_IDS = Object.freeze({
|
||||
"1": "Shoes",
|
||||
"2": "Trash can",
|
||||
"3": "Pet bowl",
|
||||
"4": "Weighing scale",
|
||||
"5": "Textiles",
|
||||
"6": "Entrapping furniture",
|
||||
"7": "Electric wire",
|
||||
"8": "Charging base",
|
||||
"9": "Feces",
|
||||
"11": "Liquid Stain",
|
||||
"12": "Solid Stain",
|
||||
"13": "Mixed Solid and Liquid Stain",
|
||||
"16": "Pet",
|
||||
"17": "Pet",
|
||||
"18": "Pedestal",
|
||||
"19": "Fall Hazard",
|
||||
"20": "Floor mirror",
|
||||
"22": "Stuck Hazard / Base",
|
||||
"24": "Power strip",
|
||||
"25": "Obstacle",
|
||||
"30": "Obstacle",
|
||||
"31": "Obstacle",
|
||||
"32": "Pet supplies",
|
||||
"33": "Obstacle",
|
||||
|
||||
"99": "Unknown Obstacle",
|
||||
|
||||
"4001": "Grain Stain",
|
||||
"4002": "Dust Stain",
|
||||
"4003": "Liquid Stain",
|
||||
"4004": "Mixed Solid and Liquid Stain",
|
||||
|
||||
"65534": "Socks",
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
DUMMY_CLIENT_CERT: DUMMY_CLIENT_CERT,
|
||||
DUMMY_CLIENT_KEY: DUMMY_CLIENT_KEY
|
||||
DUMMY_CLIENT_KEY: DUMMY_CLIENT_KEY,
|
||||
AI_OBSTACLE_IDS: AI_OBSTACLE_IDS
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
const Logger = require("../../Logger");
|
||||
const mapEntities = require("../../entities/map");
|
||||
const MideaConst = require("./MideaConst");
|
||||
const Protobufs = require("./generated/midea_protobufs.js");
|
||||
const uuid = require("uuid");
|
||||
const zlib = require("zlib");
|
||||
|
||||
class MideaMapParser {
|
||||
@ -84,6 +87,9 @@ class MideaMapParser {
|
||||
case "evt_active_zones":
|
||||
await this.handleActiveZonesUpdate(data);
|
||||
break;
|
||||
case "semantic_data":
|
||||
await this.handleSemanticDataUpdate(data);
|
||||
break;
|
||||
|
||||
case "threshold_area":
|
||||
case "points":
|
||||
@ -91,7 +97,6 @@ class MideaMapParser {
|
||||
case "user_defined_carpet":
|
||||
case "backup_map":
|
||||
case "3d":
|
||||
case "semantic_data":
|
||||
case "stain_area":
|
||||
case "partition":
|
||||
case "adjacent":
|
||||
@ -479,7 +484,7 @@ class MideaMapParser {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {import("../../msmart/dtos/MSmartActiveZonesDTO")} data
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
@ -510,6 +515,70 @@ class MideaMapParser {
|
||||
this.entities.push(...entities);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} data
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async handleSemanticDataUpdate(data) {
|
||||
this.entities = this.entities.filter(e => e.type !== mapEntities.PointMapEntity.TYPE.OBSTACLE);
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await MideaMapParser.DECOMPRESS_PAYLOAD(data);
|
||||
if (payload.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const semanticInfo = Protobufs.decodeSemanticMapInfo(payload);
|
||||
|
||||
if (!semanticInfo.objects || semanticInfo.objects.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newObstacleEntities = [];
|
||||
|
||||
for (const object of semanticInfo.objects) {
|
||||
if (!object.center_point) {
|
||||
continue;
|
||||
}
|
||||
const coords = this.convertToValetudoCoordinates(object.center_point.x, object.center_point.y);
|
||||
|
||||
const obstacleType = MideaConst.AI_OBSTACLE_IDS[object.object_type] ?? `Unknown ID ${object.object_type}`;
|
||||
const confidence = object.ai_image_info?.confidence ? `${object.ai_image_info.confidence}%` : "N/A";
|
||||
const image = object.ai_image_info?.absolute_path;
|
||||
|
||||
let objectHash;
|
||||
// field_4_data contains the BoundingBox and the unique hash.
|
||||
if (object.field_4_data && object.field_4_data.length > 1) {
|
||||
objectHash = object.field_4_data[1].toString("utf-8");
|
||||
} else {
|
||||
objectHash = `${object.timestamp_us}_${object.center_point.x}_${object.center_point.y}`;
|
||||
}
|
||||
|
||||
newObstacleEntities.push(new mapEntities.PointMapEntity({
|
||||
points: [
|
||||
coords.x,
|
||||
coords.y,
|
||||
],
|
||||
type: mapEntities.PointMapEntity.TYPE.OBSTACLE,
|
||||
metaData: {
|
||||
label: `${obstacleType} (${confidence})`,
|
||||
id: uuid.v5(objectHash, OBSTACLE_ID_NAMESPACE),
|
||||
image: image
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this.entities.push(...newObstacleEntities);
|
||||
} catch (e) {
|
||||
Logger.warn("Error while parsing semantic_data:", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} data
|
||||
@ -547,6 +616,7 @@ MideaMapParser.PATH_TYPES = Object.freeze({
|
||||
|
||||
"CLEANING_TURN": 80,
|
||||
"CLEANING": 100,
|
||||
// TODO: 120
|
||||
|
||||
"MAPPING": 170,
|
||||
"TAXIING": 180,
|
||||
@ -555,4 +625,6 @@ MideaMapParser.PATH_TYPES = Object.freeze({
|
||||
"HEADER": 2400, // Not a real type. Just the format header
|
||||
});
|
||||
|
||||
const OBSTACLE_ID_NAMESPACE = "533c87f6-c6a7-4428-9df9-347f33994348";
|
||||
|
||||
module.exports = MideaMapParser;
|
||||
|
||||
@ -117,6 +117,65 @@ class MideaQuirkFactory {
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
});
|
||||
case MideaQuirkFactory.KNOWN_QUIRKS.AI_OBSTACLE_CLASSIFICATION:
|
||||
return new Quirk({
|
||||
id: id,
|
||||
title: "AI Obstacle Classification",
|
||||
description: "Controls whether and how hard the robot should try to actually understand and work around the encountered obstacles.",
|
||||
options: ["off", "high", "normal"],
|
||||
getter: async () => {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.ACTION,
|
||||
payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_CLEANING_SETTINGS_1)
|
||||
});
|
||||
|
||||
const response = await this.robot.sendCommand(packet.toHexString());
|
||||
const parsedResponse = BEightParser.PARSE(response);
|
||||
|
||||
if (parsedResponse instanceof MSmartCleaningSettings1DTO) {
|
||||
switch (parsedResponse.ai_grade_avoidance_mode) {
|
||||
case 0:
|
||||
return "off";
|
||||
case 1:
|
||||
return "high";
|
||||
case 2:
|
||||
return "normal";
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid response from robot");
|
||||
}
|
||||
},
|
||||
setter: async (value) => {
|
||||
let val;
|
||||
|
||||
switch (value) {
|
||||
case "off":
|
||||
val = 0;
|
||||
break;
|
||||
case "high":
|
||||
val = 1;
|
||||
break;
|
||||
case "normal":
|
||||
val = 2;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid obstacle avoidance sensitivity value: ${value}`);
|
||||
}
|
||||
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x15, // super-obstacle
|
||||
val
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
});
|
||||
@ -129,6 +188,7 @@ class MideaQuirkFactory {
|
||||
MideaQuirkFactory.KNOWN_QUIRKS = {
|
||||
HAIR_CUTTING: "afa83002-87db-43bb-b8ff-e4b38863a5d3",
|
||||
HAIR_CUTTING_ONE_TIME_TURBO: "224b6a0a-1a51-48d7-9d4d-61645399d368",
|
||||
AI_OBSTACLE_CLASSIFICATION: "75af01c4-5c24-4cb3-9619-41b46b6ce333",
|
||||
};
|
||||
|
||||
module.exports = MideaQuirkFactory;
|
||||
|
||||
@ -15,6 +15,7 @@ const MideaQuirkFactory = require("./MideaQuirkFactory");
|
||||
|
||||
const capabilities = require("./capabilities");
|
||||
const QuirksCapability = require("../../core/capabilities/QuirksCapability");
|
||||
const {IMAGE_FILE_FORMAT} = require("../../utils/const");
|
||||
|
||||
const entities = require("../../entities");
|
||||
const stateAttrs = entities.state.attributes;
|
||||
@ -143,15 +144,26 @@ class MideaValetudoRobot extends ValetudoRobot {
|
||||
capabilities.MideaMopDockDryManualTriggerCapability,
|
||||
capabilities.MideaMopExtensionControlCapability,
|
||||
capabilities.MideaCameraLightControlCapability,
|
||||
capabilities.MideaObstacleAvoidanceControlCapability,
|
||||
].forEach(capability => {
|
||||
this.registerCapability(new capability({robot: this}));
|
||||
});
|
||||
|
||||
this.registerCapability(new capabilities.MideaObstacleImagesCapability({
|
||||
robot: this,
|
||||
fileFormat: IMAGE_FILE_FORMAT.JPG,
|
||||
dimensions: {
|
||||
width: 640,
|
||||
height: 480
|
||||
}
|
||||
}));
|
||||
|
||||
this.registerCapability(new QuirksCapability({
|
||||
robot: this,
|
||||
quirks: [
|
||||
quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.HAIR_CUTTING),
|
||||
quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.HAIR_CUTTING_ONE_TIME_TURBO)
|
||||
quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.HAIR_CUTTING_ONE_TIME_TURBO),
|
||||
quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.AI_OBSTACLE_CLASSIFICATION)
|
||||
]
|
||||
}));
|
||||
|
||||
|
||||
@ -0,0 +1,95 @@
|
||||
const BEightParser = require("../../../msmart/BEightParser");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO");
|
||||
const ObstacleAvoidanceControlCapability = require("../../../core/capabilities/ObstacleAvoidanceControlCapability");
|
||||
|
||||
/**
|
||||
* @extends ObstacleAvoidanceControlCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaObstacleAvoidanceControlCapability extends ObstacleAvoidanceControlCapability {
|
||||
|
||||
/**
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async isEnabled() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.ACTION,
|
||||
payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_STATUS)
|
||||
});
|
||||
|
||||
const response = await this.robot.sendCommand(packet.toHexString());
|
||||
const parsedResponse = BEightParser.PARSE(response);
|
||||
|
||||
if (parsedResponse instanceof MSmartStatusDTO) {
|
||||
return parsedResponse.ai_avoidance_switch;
|
||||
} else {
|
||||
throw new Error("Invalid response from robot");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async enable() {
|
||||
const statusPacket = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.ACTION,
|
||||
payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_STATUS)
|
||||
});
|
||||
|
||||
const response = await this.robot.sendCommand(statusPacket.toHexString());
|
||||
const status = BEightParser.PARSE(response);
|
||||
|
||||
if (!(status instanceof MSmartStatusDTO)) {
|
||||
throw new Error("Invalid status response from robot");
|
||||
}
|
||||
|
||||
if (status.ai_recognition_switch === false) {
|
||||
const tosPacket = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x0f, // AI Recognition (ToS)
|
||||
0x01 // true
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(tosPacket.toHexString());
|
||||
}
|
||||
|
||||
const avoidancePacket = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x2c, // AI Obstacle Avoidance
|
||||
0x01 // true
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(avoidancePacket.toHexString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async disable() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x2c, // AI Obstacle Avoidance
|
||||
0x00 // false
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaObstacleAvoidanceControlCapability;
|
||||
@ -0,0 +1,95 @@
|
||||
const BEightParser = require("../../../msmart/BEightParser");
|
||||
const fs = require("fs");
|
||||
const Logger = require("../../../Logger");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO");
|
||||
const ObstacleImagesCapability = require("../../../core/capabilities/ObstacleImagesCapability");
|
||||
|
||||
|
||||
/**
|
||||
* @extends ObstacleImagesCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaObstacleImagesCapability extends ObstacleImagesCapability {
|
||||
/**
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async isEnabled() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.ACTION,
|
||||
payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_STATUS)
|
||||
});
|
||||
|
||||
const response = await this.robot.sendCommand(packet.toHexString());
|
||||
const parsedResponse = BEightParser.PARSE(response);
|
||||
|
||||
if (parsedResponse instanceof MSmartStatusDTO) {
|
||||
return parsedResponse.obstacle_image_upload_switch;
|
||||
} else {
|
||||
throw new Error("Invalid response from robot");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async enable() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x35, // AI Images
|
||||
0x01 // true
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async disable() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x35, // AI Images
|
||||
0x00 // false
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} image
|
||||
* @returns {Promise<import('stream').Readable|null>}
|
||||
*/
|
||||
async getStreamForImage(image) {
|
||||
if (!/^\/userdata\/aiimgs\/[^/]+\.jpg$/.test(image)) {
|
||||
Logger.warn("Unexpected obstacle image path:", image);
|
||||
|
||||
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 = MideaObstacleImagesCapability;
|
||||
@ -18,6 +18,8 @@ module.exports = {
|
||||
MideaMopDockCleanManualTriggerCapability: require("./MideaMopDockCleanManualTriggerCapability"),
|
||||
MideaMopDockDryManualTriggerCapability: require("./MideaMopDockDryManualTriggerCapability"),
|
||||
MideaMopExtensionControlCapability: require("./MideaMopExtensionControlCapability"),
|
||||
MideaObstacleAvoidanceControlCapability: require("./MideaObstacleAvoidanceControlCapability"),
|
||||
MideaObstacleImagesCapability: require("./MideaObstacleImagesCapability"),
|
||||
MideaOperationModeControlCapability: require("./MideaOperationModeControlCapability"),
|
||||
MideaSpeakerTestCapability: require("./MideaSpeakerTestCapability"),
|
||||
MideaSpeakerVolumeControlCapability: require("./MideaSpeakerVolumeControlCapability"),
|
||||
|
||||
@ -24,7 +24,9 @@
|
||||
"ts-check": "tsc --noEmit",
|
||||
"test": "mocha \"test/**/*_spec.js\"",
|
||||
"prepare_commit": "npm run lint_fix && npm run ts-check && npm run test",
|
||||
"pre_build": "node ../util/generate_build_metadata.js",
|
||||
"generate_code": "npm run generate_midea_protobufs",
|
||||
"generate_midea_protobufs": "pbjs --es5 ./lib/robots/midea/generated/midea_protobufs.js ../assets/protobuf/midea.proto",
|
||||
"pre_build": "npm run generate_code && node ../util/generate_build_metadata.js",
|
||||
"build": "npm run build_armv7 && npm run build_aarch64 && npm run build_armv7_lowmem",
|
||||
"build_armv7": "npm run pre_build && cross-env PKG_CACHE_PATH=../build_dependencies/pkg pkg --targets node22-linuxstatic-armv7 --compress Brotli --no-bytecode --public-packages \"*\" --options \"expose-gc,max-heap-size=42\" . --output ../build/armv7/valetudo",
|
||||
"build_aarch64": "npm run pre_build && cross-env PKG_CACHE_PATH=../build_dependencies/pkg pkg --targets node22-linuxstatic-arm64 --compress Brotli --no-bytecode --public-packages \"*\" --options \"expose-gc,max-heap-size=64\" . --output ../build/aarch64/valetudo",
|
||||
|
||||
53
package-lock.json
generated
53
package-lock.json
generated
@ -25,6 +25,7 @@
|
||||
"eslint-plugin-regexp": "2.6.0",
|
||||
"eslint-plugin-sort-keys-fix": "1.1.2",
|
||||
"eslint-plugin-sort-requires": "git+https://npm@github.com/Hypfer/eslint-plugin-sort-requires.git#2.1.1",
|
||||
"pbjs": "0.0.14",
|
||||
"swagger-jsdoc": "git+https://npm@github.com/Hypfer/swagger-jsdoc.git#7.0.0-rc.6-noyaml-monorepo-fix",
|
||||
"swagger-parser": "10.0.3",
|
||||
"typescript": "4.8.4",
|
||||
@ -15927,6 +15928,28 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pbjs": {
|
||||
"version": "0.0.14",
|
||||
"resolved": "https://registry.npmjs.org/pbjs/-/pbjs-0.0.14.tgz",
|
||||
"integrity": "sha512-F4aA0ojrQ37kxFPOg4yRLP/vxb76rYQwMQigmVEljYlA7hZKmjaWjP6IkRn4nA0NdIj4Xxe4iqWrrIhJy+MwWQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"commander": "4.0.1",
|
||||
"protocol-buffers-schema": "3.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbjs": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pbjs/node_modules/commander": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz",
|
||||
"integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
@ -17432,6 +17455,12 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.1.0.tgz",
|
||||
"integrity": "sha512-1g9zFjLFhGN1Dc5UVO8D2loVslp6sVxk5sJqgD66CuWUITh2gOaTLRN/pIakGFfB6e0nNF6hImrYFDurEsA1UQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@ -33394,6 +33423,24 @@
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
|
||||
},
|
||||
"pbjs": {
|
||||
"version": "0.0.14",
|
||||
"resolved": "https://registry.npmjs.org/pbjs/-/pbjs-0.0.14.tgz",
|
||||
"integrity": "sha512-F4aA0ojrQ37kxFPOg4yRLP/vxb76rYQwMQigmVEljYlA7hZKmjaWjP6IkRn4nA0NdIj4Xxe4iqWrrIhJy+MwWQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"commander": "4.0.1",
|
||||
"protocol-buffers-schema": "3.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz",
|
||||
"integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
@ -34276,6 +34323,12 @@
|
||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.4.0.tgz",
|
||||
"integrity": "sha512-9t5qARVofg2xQqKtytzt+lZ4d1Qvj8t5B8fEwXK6qOfgRLgH/b13QlgEyDh033NOS31nXeFbYv7CLUDG1CeifQ=="
|
||||
},
|
||||
"protocol-buffers-schema": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.1.0.tgz",
|
||||
"integrity": "sha512-1g9zFjLFhGN1Dc5UVO8D2loVslp6sVxk5sJqgD66CuWUITh2gOaTLRN/pIakGFfB6e0nNF6hImrYFDurEsA1UQ==",
|
||||
"dev": true
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
"eslint-plugin-regexp": "2.6.0",
|
||||
"eslint-plugin-sort-keys-fix": "1.1.2",
|
||||
"eslint-plugin-sort-requires": "git+https://npm@github.com/Hypfer/eslint-plugin-sort-requires.git#2.1.1",
|
||||
"pbjs": "0.0.14",
|
||||
"swagger-jsdoc": "git+https://npm@github.com/Hypfer/swagger-jsdoc.git#7.0.0-rc.6-noyaml-monorepo-fix",
|
||||
"swagger-parser": "10.0.3",
|
||||
"typescript": "4.8.4",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user