From e2ed176381a0b4561125b7cf163b089e8c95366a Mon Sep 17 00:00:00 2001 From: Erinome Date: Mon, 8 Aug 2022 22:45:56 +0300 Subject: [PATCH] add new mqtt command and REST API for manual movement control --- lib/MqttClient.js | 29 ++++++++++++++++-- lib/miio/Vacuum.js | 60 ++++++++++++++++++++++++++++++++++++++ lib/webserver/WebServer.js | 12 ++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/lib/MqttClient.js b/lib/MqttClient.js index 10dd063..082b452 100644 --- a/lib/MqttClient.js +++ b/lib/MqttClient.js @@ -22,7 +22,8 @@ const CUSTOM_COMMANDS = { STORE_MAP: "store_map", GET_DESTINATIONS: "get_destinations", PLAY_SOUND: "play_sound", - SET_WATER_GRADE: "set_water_grade" + SET_WATER_GRADE: "set_water_grade", + SEND_RC_COMMAND: "remote_control" }; //TODO: since this is also displayed in the UI it should be moved somewhere else @@ -821,9 +822,9 @@ MqttClient.prototype.handleCustomCommand = function (message) { case CUSTOM_COMMANDS.SET_WATER_GRADE: if (this.vacuum.features.water_usage_ctrl) { if (Object.keys(WATER_GRADES).includes(msg.grade)) { - this.vacuum.setWaterGrade(WATER_GRADES[msg.grade], (err, data) => this.publishCommandStatus("set_water_grade",data,err)); + this.vacuum.setWaterGrade(WATER_GRADES[msg.grade], (err, data) => this.publishCommandStatus(msg.command,data,err)); } else if (parseInt(msg.grade)) { - this.vacuum.setWaterGrade(parseInt(msg.grade), (err, data) => this.publishCommandStatus("set_water_grade",data,err)); + this.vacuum.setWaterGrade(parseInt(msg.grade), (err, data) => this.publishCommandStatus(msg.command,data,err)); } else { this.publishCommandStatus(msg.command,null,"Unsupported water grade value specified."); } @@ -831,6 +832,28 @@ MqttClient.prototype.handleCustomCommand = function (message) { this.publishCommandStatus(msg.command,null,"Setting water grade is not supported on this device."); } break; + /** + * send a RC mode command to the device + * starting and stopping RC mode is done automatically, you can try sending multiple commands in a row + * { + * "command": "remote_control", + * "angle": -1.3, (between -3.14 and 3.14) + * "velocity": 0.0, (between -0.3 and 0.3) + * "duration": 1000 (in ms) + * "startdelay": 7500 (optional, in ms) + * } + * startdelay is the delay the device needs to wait after starting RC mode to be able to actually perform RC commands (default: 8 seconds, maybe your device would be faster) + * see https://github.com/marcelrv/XiaomiRobotVacuumProtocol/blob/master/rc.md for other parameters which are passed directly to `app_rc_move` miio command + */ + case CUSTOM_COMMANDS.SEND_RC_COMMAND: + if (msg.angle !== undefined && !isNaN(parseFloat(msg.angle)) && + msg.velocity !== undefined && !isNaN(parseFloat(msg.velocity)) && + msg.duration !== undefined && !isNaN(parseInt(msg.duration))) { + this.vacuum.autoManualControl(parseFloat(msg.angle), parseFloat(msg.velocity), parseInt(msg.duration), parseInt(msg.startdelay) || null, (err, data) => this.publishCommandStatus(msg.command,data,err)); + } else { + this.publishCommandStatus(msg.command,null,"Invalid args supplied."); + } + break; default: this.publishCommandStatus(msg.command,null,"Received invalid custom command: " + JSON.stringify(msg)); } diff --git a/lib/miio/Vacuum.js b/lib/miio/Vacuum.js index d08a396..4974bf0 100644 --- a/lib/miio/Vacuum.js +++ b/lib/miio/Vacuum.js @@ -40,6 +40,11 @@ const Vacuum = function(valetudo) { this.handshakeInProgress = false; this.handshakeTimeout = null; + this.autoRCTimer = 0; + this.autoRCDelayed = false; + this.autoRCQueue = []; + this.autoRCSeq = 0; + this.idleTimeoutTimer = 0; this.resetQueues(0xff); @@ -389,6 +394,61 @@ Vacuum.prototype.setManualControl = function(angle, velocity, duration, sequence this.sendMessage("app_rc_move", [{"omega": angle, "velocity": velocity, "seqnum": sequenceId, "duration": duration}], {}, callback) }; +Vacuum.prototype.autoManualControl = function(angle, velocity, duration, startdelay, callback) { + const self = this; + const sendCommand = function(angle, velocity, duration, startdelay, callback) { + self.autoRCDelayed = true; + clearTimeout(self.autoRCTimer); + self.autoRCTimer = setTimeout(() => { + self.stopManualControl(err => { + self.autoRCTimer = 0; + }); + },5e3+duration); + self.setManualControl(angle, velocity, duration, self.autoRCSeq++, (err,res) => { + callback(err,res); + setTimeout(() => { + if (self.autoRCQueue.length) { + let args = self.autoRCQueue.shift(); + sendCommand(...args); + } else { + self.autoRCDelayed = false; + } + },5e2+duration); + }); + + }; + this.getCurrentStatus((err,res) => { + if (err) { + return callback(err); + } + if (res.state === 7 || self.autoRCDelayed) { // already in RC mode or waiting to enter it + if (!self.autoRCDelayed) { + // process immediately if there's no delay + sendCommand(angle, velocity, duration, startdelay, callback); + } else { + // or add the request to RC queue to run it later + clearTimeout(self.autoRCTimer); + self.autoRCQueue.push([angle, velocity, duration, startdelay, callback]); + } + } else if ([2,3,10,12].includes(res.state)) { // idle, sleep, ... + self.autoRCDelayed = true; + self.startManualControl(err => { + if (err) { + self.autoRCDelayed = false; + return callback(err); + } + // apparently sequence number MUST start with 1 (when 0 it is ignored) + self.autoRCSeq = 1; + // we need to use some LONG ENOUGH delay to wait after starting remote control mode + // since otherwise the command seems to be discarded + setTimeout(() => sendCommand(angle, velocity, duration, startdelay, callback), startdelay || 8e3); + }); + } else { + callback("device is at inappropriate state to run autoRC commands (acceptable: 2,3,10,12, now: " + res.state + ")"); + } + }); +}; + /** * Returns carpet detection parameter like * { diff --git a/lib/webserver/WebServer.js b/lib/webserver/WebServer.js index 3a15b6f..bf0b1b6 100644 --- a/lib/webserver/WebServer.js +++ b/lib/webserver/WebServer.js @@ -1430,6 +1430,18 @@ const WebServer = function (valetudo) { } }); + this.app.put("/api/auto_manual_control", function (req, res) { + if (req.body && req.body.angle !== undefined && req.body.velocity !== undefined && req.body.duration !== undefined) { + self.vacuum.autoManualControl(req.body.angle, req.body.velocity, req.body.duration, parseInt(req.body.startdelay) || null, function (err, data) { + if (err) { + res.status(500).send(err.toString()); + } else { + res.json(data); + } + }); + } + }); + this.app.get("/api/carpet_mode", function (req, res) { self.vacuum.getCarpetMode(function (err, data) { if (err) {