From 69be9df7f2117999ccf6e57e1dcc8444b2ce8bca Mon Sep 17 00:00:00 2001 From: Hypfer Date: Fri, 22 Mar 2019 22:12:48 +0100 Subject: [PATCH] #43 MQTT Support + Minor cleanups --- .travis.yml | 2 +- README.md | 2 + index.js | 2 +- lib/Configuration.js | 71 +++ lib/MqttClient.js | 230 ++++++++ lib/Tools.js | 16 + Valetudo.js => lib/Valetudo.js | 22 +- {miio => lib/miio}/Codec.js | 0 {miio => lib/miio}/HandshakePacket.js | 0 {miio => lib/miio}/Stamp.js | 0 {miio => lib/miio}/Vacuum.js | 28 +- {webserver => lib/webserver}/WebServer.js | 643 +++++++++++----------- package.json | 5 +- 13 files changed, 670 insertions(+), 351 deletions(-) create mode 100644 lib/Configuration.js create mode 100644 lib/MqttClient.js create mode 100644 lib/Tools.js rename Valetudo.js => lib/Valetudo.js (65%) rename {miio => lib/miio}/Codec.js (100%) rename {miio => lib/miio}/HandshakePacket.js (100%) rename {miio => lib/miio}/Stamp.js (100%) rename {miio => lib/miio}/Vacuum.js (96%) rename {webserver => lib/webserver}/WebServer.js (68%) diff --git a/.travis.yml b/.travis.yml index 372744f7..a875689f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ install: - npm install script: -- "./node_modules/.bin/pkg --targets latest-linux-armv7 --no-bytecode --options max-old-space-size=72 --public-packages=exif-parser,omggif,trim,prettycron ." +- "./node_modules/.bin/pkg --targets latest-linux-armv7 --no-bytecode --options max-old-space-size=72 --public-packages=exif-parser,omggif,trim,prettycron,mqtt ." deploy: provider: releases diff --git a/README.md b/README.md index f93e830b..3bb1eaab 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ It runs directly on the vacuum and requires no cloud connection whatsoever. * Go-To * Zoned Cleanup * Configure Timers +* MQTT +* MQTT HomeAssistant Autodiscovery * Start/Stop/Pause Robot * Find Robot/Send robot to charging dock * Power settings diff --git a/index.js b/index.js index a41bae38..1696cd87 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,2 @@ -const Valetudo = require("./Valetudo"); +const Valetudo = require("./lib/Valetudo"); new Valetudo(); \ No newline at end of file diff --git a/lib/Configuration.js b/lib/Configuration.js new file mode 100644 index 00000000..e690db64 --- /dev/null +++ b/lib/Configuration.js @@ -0,0 +1,71 @@ +const fs = require("fs"); +const Tools = require("./Tools"); +const path = require("path"); + +/** + * @constructor + */ +const Configuration = function() { + this.location = process.env.VALETUDO_CONFIG ? process.env.VALETUDO_CONFIG : "/mnt/data/valetudo/config.json"; + this.settings = { + "spots": [], + "areas": [], + "mqtt" : { + enabled: false, + identifier: "rockrobo", + broker_url: "mqtt://foobar.example" + } + }; + + /* load an existing configuration file. if it is not present, create it using the default configuration */ + if(fs.existsSync(this.location)) { + console.log("Loading configuration file:", this.location); + + try { + this.settings = JSON.parse(fs.readFileSync(this.location)); + } catch(e) { + //TODO: handle this + console.error("Invalid configuration file!"); + throw e; + } + } else { + console.log("No configuration file present. Creating one at:", this.location); + Tools.MK_DIR_PATH(path.dirname(this.location)); + this.persist(); + } +}; + + +/** + * + * @param key {string} + * @returns {*} + */ +Configuration.prototype.get = function(key) { + return this.settings[key]; +}; + +Configuration.prototype.getAll = function() { + return this.settings; +}; + +/** + * + * @param key {string} + * @param val {string} + */ +Configuration.prototype.set = function(key, val) { + this.settings[key] = val; + + this.persist(); +}; + +Configuration.prototype.persist = function() { + fs.writeFile(this.location, JSON.stringify(this.settings, null, 2), (err) => { + if (err) { + console.error(err); + } + }); +}; + +module.exports = Configuration; \ No newline at end of file diff --git a/lib/MqttClient.js b/lib/MqttClient.js new file mode 100644 index 00000000..4eb72a21 --- /dev/null +++ b/lib/MqttClient.js @@ -0,0 +1,230 @@ +const mqtt = require("mqtt"); + + +const COMMANDS = { + turn_on: "turn_on", + return_to_base: "return_to_base", + stop: "stop", + clean_spot: "clean_spot", + locate: "locate", + start_pause: "start_pause", + set_fan_speed: "set_fan_speed" +}; + +//TODO: since this is also displayed in the UI it should be moved somewhere else +const FAN_SPEEDS = { + min: 38, + medium: 60, + high: 75, + max: 100 +}; + + + +/** + * + * @param options {object} + * @param options.vacuum {Vacuum} + * @param options.brokerURL {string} + * @param options.identifier {string} + * @constructor + */ +const MqttClient = function(options) { + this.vacuum = options.vacuum; + this.brokerURL = options.brokerURL; + this.identifier = options.identifier || "rockrobo"; + + this.topics = { + command: "valetudo/" + this.identifier + "/command", + set_fan_speed: "valetudo/" + this.identifier + "/set_fan_speed", + send_command: "valetudo/" + this.identifier + "/custom_command", + state: "valetudo/" + this.identifier + "/state", + homeassistant_autoconf: "homeassistant/vacuum/valetudo_" + this.identifier + "/config" + }; + + this.connect(); + this.updateStateTopic(); +}; + +MqttClient.prototype.connect = function() { + if(!this.client || (this.client && this.client.connected === false && this.client.reconnecting === false)) { + this.client = mqtt.connect(this.brokerURL); + + this.client.on("connect", () => { + this.client.subscribe([ + this.topics.command, + this.topics.set_fan_speed + ], err => { + if(!err) { + this.client.publish(this.topics.homeassistant_autoconf, JSON.stringify({ + name: this.identifier, + supported_features: [ + "turn_on", + "pause", + "stop", + "return_home", + "battery", + "status", + "locate", + "clean_spot", + "fan_speed", + "send_command" + ], + command_topic: this.topics.command, + battery_level_topic: this.topics.state, + battery_level_template: "{{ value_json.battery_level }}", + charging_topic: this.topics.state, + charging_template: "{{value_json.charging}}", + cleaning_topic: this.topics.state, + cleaning_template: "{{value_json.cleaning}}", + docked_topic: this.topics.state, + docked_template: "{{value_json.docked}}", + error_topic: this.topics.state, + error_template: "{{value_json.error}}", + fan_speed_topic: this.topics.state, + fan_speed_template: "{{ value_json.fan_speed }}", + set_fan_speed_topic: this.topics.set_fan_speed, + fan_speed_list: [ + "min", + "medium", + "high", + "max" + ], + send_command_topic: this.topics.send_command + + }), { + retain: true + }) + } else { + //TODO: needs more error handling + console.error(err); + } + }); + }); + + this.client.on("message", (topic, message) => { + this.handleCommand(topic, message.toString()); + }) + } +}; + +MqttClient.prototype.updateStateTopic = function() { + if(this.stateUpdateTimeout) { + clearTimeout(this.stateUpdateTimeout); + } + + if(this.client && this.client.connected === true) { + this.vacuum.getCurrentStatus((err, res) => { + if(!err) { + var response = {}; + + response.battery_level = res.battery; + response.docked = [8,14].indexOf(res.state) !== -1; + response.cleaning = res.in_cleaning === 1; + response.charging = res.state === 8; + + switch(res.fan_power) { + case FAN_SPEEDS.min: + response.fan_speed = "min"; + break; + case FAN_SPEEDS.medium: + response.fan_speed = "medium"; + break; + case FAN_SPEEDS.high: + response.fan_speed = "high"; + break; + case FAN_SPEEDS.max: + response.fan_speed = "max"; + break; + default: + response.fan_speed = res.fan_power; + } + + if(res.error_code !== 0) { + response.error = res.human_error; + } + + this.client.publish(this.topics.state, JSON.stringify(response)); + this.stateUpdateTimeout = setTimeout(() => { + this.updateStateTopic() + }, 10000); + } else { + console.error(err); + this.stateUpdateTimeout = setTimeout(() => { + this.updateStateTopic() + }, 30000); + } + }) + } else { + this.stateUpdateTimeout = setTimeout(() => { + this.updateStateTopic() + }, 30000); + } +}; + +/** + * @param topic {string} + * @param command {string} + */ +MqttClient.prototype.handleCommand = function(topic, command) { + var param; + if(topic === this.topics.set_fan_speed) { + param = command; + command = COMMANDS.set_fan_speed; + } + + switch(command) { //TODO: error handling + case COMMANDS.turn_on: + this.vacuum.startCleaning(() => { + this.updateStateTopic(); + }); + break; + case COMMANDS.stop: + this.vacuum.stopCleaning(() => { + this.updateStateTopic(); + }); + break; + case COMMANDS.return_to_base: + this.vacuum.stopCleaning(() => { + this.vacuum.driveHome(() => { + this.updateStateTopic(); + }); + }); + break; + case COMMANDS.clean_spot: + this.vacuum.spotClean(() => { + this.updateStateTopic(); + }); + break; + case COMMANDS.locate: + this.vacuum.findRobot(() => { + this.updateStateTopic(); + }); + break; + case COMMANDS.start_pause: + this.vacuum.getCurrentStatus((err, res) => { + if(!err) { + if(res.in_cleaning === 1 && [5,11,17].indexOf(res.state) !== -1) { + this.vacuum.pauseCleaning(() => { + this.updateStateTopic(); + }); + } else { + this.vacuum.startCleaning(() => { + this.updateStateTopic(); + }); + } + } + }); + break; + case COMMANDS.set_fan_speed: + this.vacuum.setFanSpeed(FAN_SPEEDS[param], () => { + this.updateStateTopic(); + }); + break; + default: + this.updateStateTopic(); + } + +}; + +module.exports = MqttClient; diff --git a/lib/Tools.js b/lib/Tools.js new file mode 100644 index 00000000..d56e3fbd --- /dev/null +++ b/lib/Tools.js @@ -0,0 +1,16 @@ +const fs = require("fs"); +const path = require("path"); + +const Tools = { + MK_DIR_PATH : function(filepath) { + var dirname = path.dirname(filepath); + if (!fs.existsSync(dirname)) { + Tools.MK_DIR_PATH(dirname); + } + if (!fs.existsSync(filepath)) { + fs.mkdirSync(filepath); + } + } +}; + +module.exports = Tools; \ No newline at end of file diff --git a/Valetudo.js b/lib/Valetudo.js similarity index 65% rename from Valetudo.js rename to lib/Valetudo.js index da505979..30cc898f 100644 --- a/Valetudo.js +++ b/lib/Valetudo.js @@ -1,10 +1,11 @@ const fs = require("fs"); const Vacuum = require("./miio/Vacuum"); const Webserver = require("./webserver/WebServer"); +const MqttClient = require("./MqttClient"); +const Configuration = require("./Configuration"); -const defaultConfigFileLocation = "/mnt/data/valetudo/config.json" - -const Valetudo = function() { +const Valetudo = function() { + this.configuration = new Configuration(); this.address = process.env.VAC_ADDRESS ? process.env.VAC_ADDRESS : "127.0.0.1"; if(process.env.VAC_TOKEN) { @@ -17,7 +18,6 @@ const Valetudo = function() { this.webPort = process.env.VAC_WEBPORT ? parseInt(process.env.VAC_WEBPORT) : 80; - this.configFileLocation = process.env.VALETUDO_CONFIG ? process.env.VALETUDO_CONFIG : defaultConfigFileLocation; this.vacuum = new Vacuum({ ip: this.address, @@ -27,11 +27,19 @@ const Valetudo = function() { this.webserver = new Webserver({ vacuum: this.vacuum, port: this.webPort, - configFileLocation: this.configFileLocation - }) -}; + configuration: this.configuration + }); + if(this.configuration.get("mqtt") && this.configuration.get("mqtt").enabled === true) { + this.mqttClient = new MqttClient({ + vacuum: this.vacuum, + brokerURL: this.configuration.get("mqtt").broker_url, + identifier: this.configuration.get("mqtt").identifier + }); + } +}; + Valetudo.NATIVE_TOKEN_PROVIDER = function() { const token = fs.readFileSync("/mnt/data/miio/device.token"); if(token && token.length >= 16) { diff --git a/miio/Codec.js b/lib/miio/Codec.js similarity index 100% rename from miio/Codec.js rename to lib/miio/Codec.js diff --git a/miio/HandshakePacket.js b/lib/miio/HandshakePacket.js similarity index 100% rename from miio/HandshakePacket.js rename to lib/miio/HandshakePacket.js diff --git a/miio/Stamp.js b/lib/miio/Stamp.js similarity index 100% rename from miio/Stamp.js rename to lib/miio/Stamp.js diff --git a/miio/Vacuum.js b/lib/miio/Vacuum.js similarity index 96% rename from miio/Vacuum.js rename to lib/miio/Vacuum.js index 36ef512b..b24dd1a7 100644 --- a/miio/Vacuum.js +++ b/lib/miio/Vacuum.js @@ -204,7 +204,18 @@ Vacuum.prototype.getCarpetMode = function(callback) { }; Vacuum.prototype.setCarpetMode = function(enable, current_integral, current_low, current_high, stall_time , callback) { - this.sendMessage("set_carpet_mode", [{"enable": (enable===true?1:0), "stall_time": parseInt(stall_time), "current_low": parseInt(current_low), "current_high": parseInt(current_high), "current_integral": parseInt(current_integral)}], {}, callback) + this.sendMessage( + "set_carpet_mode", + [{ + "enable": (enable === true ? 1 : 0), + "stall_time": parseInt(stall_time), + "current_low": parseInt(current_low), + "current_high": parseInt(current_high), + "current_integral": parseInt(current_integral) + }], + {}, + callback + ); }; /** @@ -347,6 +358,7 @@ Vacuum.prototype.getTimezone = function(callback) { /** * Set Timezone * @param new_zone new timezone + * @param callback {function} */ Vacuum.prototype.setTimezone = function(new_zone, callback) { this.sendMessage("set_timezone", [new_zone], {}, callback); @@ -412,8 +424,8 @@ Vacuum.prototype.getCurrentStatus = function(callback) { if(err) { callback(err); } else { - res.human_state = Vacuum.STATES[res.state]; - res.human_error = Vacuum.ERROR_CODES[res.error_code]; + res.human_state = Vacuum.getStateCodeDescription(res.state); + res.human_error = Vacuum.getErrorCodeDescription[res.error_code]; delete(res["msg_seq"]); callback(null, res); @@ -473,7 +485,7 @@ Vacuum.prototype.getCleanSummary = function(callback) { */ Vacuum.prototype.getCleanRecord = function (recordId, callback) { this.sendMessage("get_clean_record", [parseInt(recordId)], {}, callback); -} +}; /* Some words on coordinates for goTo and startCleaningZone: For the vacuum, everything is relative to the point 25500/25500 which is the docking station. Units are mm. @@ -543,21 +555,21 @@ Vacuum.GET_ARRAY_HANDLER = function(callback) { } }; -Vacuum.prototype.getErrorCodeDescription = function(errorCodeId) { +Vacuum.getErrorCodeDescription = function(errorCodeId) { if (Vacuum.ERROR_CODES.hasOwnProperty(errorCodeId)) { return Vacuum.ERROR_CODES[errorCodeId]; } else { return "UNKNOWN ERROR CODE"; } -} +}; -Vacuum.prototype.getStateCodeDescription = function(stateCodeId) { +Vacuum.getStateCodeDescription = function(stateCodeId) { if (Vacuum.STATES.hasOwnProperty(stateCodeId)) { return Vacuum.STATES[stateCodeId]; } else { return "UNKNOWN STATE CODE"; } -} +}; Vacuum.STATES = { 1: "Starting", diff --git a/webserver/WebServer.js b/lib/webserver/WebServer.js similarity index 68% rename from webserver/WebServer.js rename to lib/webserver/WebServer.js index 74da14a8..ccc47fd8 100644 --- a/webserver/WebServer.js +++ b/lib/webserver/WebServer.js @@ -9,64 +9,36 @@ const bodyParser = require("body-parser"); const Jimp = require("jimp"); const url = require("url"); -const MapFunctions = require("../client/js/MapFunctions"); +const MapFunctions = require("../../client/js/MapFunctions"); //assets -const chargerImagePath = path.join(__dirname, '../client/img/charger.png'); -const robotImagePath = path.join(__dirname, '../client/img/robot.png'); - -const defaultConfigFileLocation = "/mnt/data/valetudo/config.json" +const chargerImagePath = path.join(__dirname, '../../client/img/charger.png'); +const robotImagePath = path.join(__dirname, '../../client/img/robot.png'); /** * * @param options * @param options.vacuum {Vacuum} * @param options.port {number} - * @param options.configFileLocation {configFileLocation} + * @param options.configuration {Configuration} * @constructor */ -const WebServer = function(options) { +const WebServer = function (options) { const self = this; this.vacuum = options.vacuum; this.port = options.port; - this.configFileLocation = options.configFileLocation; - - /* this is the default configuration */ - this.configuration = {"spots": [], - "areas": [] }; + this.configuration = options.configuration; this.app = express(); this.app.use(compression()); this.app.use(bodyParser.json()); - function writeConfigToFile(){ - fs.writeFile(self.configFileLocation, JSON.stringify(self.configuration), (err) => { - if (err) { - console.error(err); - return; - }; - }); - } - - /* load an existing configuration file. if it is not present, create it using the default configuration */ - if(fs.existsSync(this.configFileLocation)) { - console.log("Loading configuration file:", this.configFileLocation) - var contents = fs.readFileSync(this.configFileLocation) - this.configuration = JSON.parse(contents); - } else { - console.log("No configuration file present. Creating one at:", this.configFileLocation) - WebServer.MK_DIR_PATH(path.dirname(this.configFileLocation)); - writeConfigToFile(); - } - - // I don't know a better way to get the configuration in scope for static methods... - WebServer.configuration = this.configuration; - this.app.get("/api/current_status", function(req,res) { - self.vacuum.getCurrentStatus(function(err,data){ - if(err) { + this.app.get("/api/current_status", function (req, res) { + self.vacuum.getCurrentStatus(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -74,13 +46,13 @@ const WebServer = function(options) { }); }); - this.app.get("/api/consumable_status", function(req,res) { - self.vacuum.getConsumableStatus(function(err,data){ - if(err) { + this.app.get("/api/consumable_status", function (req, res) { + self.vacuum.getConsumableStatus(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { - self.vacuum.getCleanSummary(function(err,data2){ - if(err) { + self.vacuum.getCleanSummary(function (err, data2) { + if (err) { res.status(500).send(err.toString()); } else { res.json({ @@ -93,9 +65,9 @@ const WebServer = function(options) { }); }); - this.app.get("/api/get_fw_version", function(req,res){ - fs.readFile("/etc/os-release", function(err,data){ - if(err) { + this.app.get("/api/get_fw_version", function (req, res) { + fs.readFile("/etc/os-release", function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { const extractedOsRelease = data.toString().match(WebServer.OS_RELEASE_FW_REGEX); @@ -119,17 +91,17 @@ const WebServer = function(options) { }); }); - this.app.get("/api/get_app_locale", function(req,res){ - self.vacuum.getAppLocale(function(err,data){ - if(err) { + this.app.get("/api/get_app_locale", function (req, res) { + self.vacuum.getAppLocale(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); } }); - }); + }); - this.app.get("/api/wifi_status", function(req,res){ + this.app.get("/api/wifi_status", function (req, res) { /* root@rockrobo:~# iw Usage: iw [options] command @@ -151,9 +123,9 @@ const WebServer = function(options) { }; const iwOutput = spawnSync("iw", ["dev", "wlan0", "link"]).stdout; - if(iwOutput) { + if (iwOutput) { const extractedWifiData = iwOutput.toString().match(WebServer.WIFI_CONNECTED_IW_REGEX); - if(extractedWifiData) { + if (extractedWifiData) { wifiConnection.connected = true; wifiConnection.connection_info.bssid = extractedWifiData[1]; @@ -167,9 +139,9 @@ const WebServer = function(options) { res.json(wifiConnection); }); - this.app.get("/api/timers", function(req,res) { - self.vacuum.getTimers(function(err,data){ - if(err) { + this.app.get("/api/timers", function (req, res) { + self.vacuum.getTimers(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -177,10 +149,10 @@ const WebServer = function(options) { }); }); - this.app.post("/api/timers", function(req,res){ - if(req.body && req.body.cron) { - self.vacuum.addTimer(req.body.cron, function(err,data){ - if(err) { + this.app.post("/api/timers", function (req, res) { + if (req.body && req.body.cron) { + self.vacuum.addTimer(req.body.cron, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -191,10 +163,10 @@ const WebServer = function(options) { } }); - this.app.put("/api/timers/:timerID", function(req,res){ - if(req.body && req.body.enabled !== undefined) { - self.vacuum.toggleTimer(req.params.timerID, req.body.enabled, function(err,data){ - if(err) { + this.app.put("/api/timers/:timerID", function (req, res) { + if (req.body && req.body.enabled !== undefined) { + self.vacuum.toggleTimer(req.params.timerID, req.body.enabled, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -205,9 +177,9 @@ const WebServer = function(options) { } }); - this.app.delete("/api/timers/:timerID", function(req,res){ - self.vacuum.deleteTimer(req.params.timerID, function(err,data){ - if(err) { + this.app.delete("/api/timers/:timerID", function (req, res) { + self.vacuum.deleteTimer(req.params.timerID, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -215,9 +187,9 @@ const WebServer = function(options) { }) }); - this.app.get("/api/get_dnd", function(req,res){ - self.vacuum.getDndTimer(function(err,data){ - if(err) { + this.app.get("/api/get_dnd", function (req, res) { + self.vacuum.getDndTimer(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -225,10 +197,10 @@ const WebServer = function(options) { }); }); - this.app.post("/api/set_dnd", function(req,res){ - if(req.body && req.body.start_hour && req.body.start_minute && req.body.end_hour && req.body.end_minute) { - self.vacuum.setDndTimer(req.body.start_hour, req.body.start_minute, req.body.end_hour, req.body.end_minute, function(err,data){ - if(err) { + this.app.post("/api/set_dnd", function (req, res) { + if (req.body && req.body.start_hour && req.body.start_minute && req.body.end_hour && req.body.end_minute) { + self.vacuum.setDndTimer(req.body.start_hour, req.body.start_minute, req.body.end_hour, req.body.end_minute, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -239,9 +211,9 @@ const WebServer = function(options) { } }); - this.app.put("/api/delete_dnd", function(req,res){ - self.vacuum.deleteDndTimer(function(err,data){ - if(err) { + this.app.put("/api/delete_dnd", function (req, res) { + self.vacuum.deleteDndTimer(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -249,9 +221,9 @@ const WebServer = function(options) { }) }); - this.app.get("/api/get_timezone", function(req,res){ - self.vacuum.getTimezone(function(err,data){ - if(err) { + this.app.get("/api/get_timezone", function (req, res) { + self.vacuum.getTimezone(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -259,10 +231,10 @@ const WebServer = function(options) { }); }); - this.app.post("/api/set_timezone", function(req,res){ - if(req.body && req.body.new_zone ) { - self.vacuum.setTimezone(req.body.new_zone, function(err,data){ - if(err) { + this.app.post("/api/set_timezone", function (req, res) { + if (req.body && req.body.new_zone) { + self.vacuum.setTimezone(req.body.new_zone, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -273,10 +245,10 @@ const WebServer = function(options) { } }); - this.app.get("/api/clean_summary", function(req,res){ - if(req.body) { - self.vacuum.getCleanSummary(function(err,data){ - if(err) { + this.app.get("/api/clean_summary", function (req, res) { + if (req.body) { + self.vacuum.getCleanSummary(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -287,12 +259,14 @@ const WebServer = function(options) { } }); - this.app.put("/api/clean_record", function(req,res){ - if(req.body && req.body.recordId) { - self.vacuum.getCleanRecord(req.body.recordId, function(err,data){ - if(err) { + this.app.put("/api/clean_record", function (req, res) { + if (req.body && req.body.recordId) { + self.vacuum.getCleanRecord(req.body.recordId, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { + //TODO: validate data from robot. Don't just hope that the array contains what we expect + //TODO: Maybe move validation to Vacuum.js and trust the data here /* * Positions in array: * 0: startTS(sec) @@ -306,10 +280,10 @@ const WebServer = function(options) { startTime: data[0][0] * 1000, //convert to ms endTime: data[0][1] * 1000, //convert to ms duration: data[0][2], - area : data[0][3], + area: data[0][3], errorCode: data[0][4], errorDescription: self.vacuum.getErrorCodeDescription(data[0][4]), - finishedFlag: (data[0][5]==1) + finishedFlag: (data[0][5] === 1) }); } }) @@ -318,9 +292,9 @@ const WebServer = function(options) { } }); - this.app.put("/api/start_cleaning", function(req,res){ - self.vacuum.startCleaning(function(err,data){ - if(err) { + this.app.put("/api/start_cleaning", function (req, res) { + self.vacuum.startCleaning(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -328,9 +302,9 @@ const WebServer = function(options) { }); }); - this.app.put("/api/pause_cleaning", function(req,res){ - self.vacuum.pauseCleaning(function(err,data){ - if(err) { + this.app.put("/api/pause_cleaning", function (req, res) { + self.vacuum.pauseCleaning(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -338,9 +312,9 @@ const WebServer = function(options) { }); }); - this.app.put("/api/stop_cleaning", function(req,res){ - self.vacuum.stopCleaning(function(err,data){ - if(err) { + this.app.put("/api/stop_cleaning", function (req, res) { + self.vacuum.stopCleaning(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -348,10 +322,10 @@ const WebServer = function(options) { }); }); - this.app.put("/api/go_to", function(req,res) { - if(req.body && req.body.x !== undefined && req.body.y !== undefined) { - self.vacuum.goTo(req.body.x, req.body.y, function(err,data) { - if(err) { + this.app.put("/api/go_to", function (req, res) { + if (req.body && req.body.x !== undefined && req.body.y !== undefined) { + self.vacuum.goTo(req.body.x, req.body.y, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -362,10 +336,10 @@ const WebServer = function(options) { } }); - this.app.put("/api/start_cleaning_zone", function(req,res) { - if(req.body) { - self.vacuum.startCleaningZone(req.body, function(err,data) { - if(err) { + this.app.put("/api/start_cleaning_zone", function (req, res) { + if (req.body) { + self.vacuum.startCleaningZone(req.body, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -376,25 +350,28 @@ const WebServer = function(options) { } }); - this.app.get("/api/get_config", function(req,res) { - res.json(self.configuration); + this.app.get("/api/get_config", function (req, res) { + res.json(self.configuration.getAll()); }); - this.app.put("/api/set_config", function(req,res) { - if(req.body && req.body.config) { - self.configuration = req.body.config; - writeConfigToFile(); - WebServer.configuration = self.configuration; - res.json('OK'); + this.app.put("/api/set_config", function (req, res) { + if (req.body && req.body.config) { + + //TODO: Validate input. Don't just blindly accept arbitrary json + Object.keys(req.body.config).forEach(function (key) { + self.configuration.set(key, req.body.config[key]); + }); + + res.sendStatus(200); } else { res.status(400).send("config missing"); } }); - this.app.put("/api/fanspeed", function(req,res) { - if(req.body && req.body.speed && req.body.speed <= 105 && req.body.speed >= 0) { - self.vacuum.setFanSpeed(req.body.speed, function(err,data) { - if(err) { + this.app.put("/api/fanspeed", function (req, res) { + if (req.body && req.body.speed && req.body.speed <= 105 && req.body.speed >= 0) { + self.vacuum.setFanSpeed(req.body.speed, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -405,10 +382,10 @@ const WebServer = function(options) { } }); - this.app.put("/api/set_sound_volume", function(req,res) { - if(req.body && req.body.volume && req.body.volume <= 100 && req.body.volume >= 0) { - self.vacuum.setSoundVolume(req.body.volume, function(err,data) { - if(err) { + this.app.put("/api/set_sound_volume", function (req, res) { + if (req.body && req.body.volume && req.body.volume <= 100 && req.body.volume >= 0) { + self.vacuum.setSoundVolume(req.body.volume, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -419,9 +396,9 @@ const WebServer = function(options) { } }); - this.app.get("/api/get_sound_volume", function(req,res) { - self.vacuum.getSoundVolume(function(err,data){ - if(err) { + this.app.get("/api/get_sound_volume", function (req, res) { + self.vacuum.getSoundVolume(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -429,9 +406,9 @@ const WebServer = function(options) { }); }); - this.app.put("/api/test_sound_volume", function(req,res){ - self.vacuum.testSoundVolume(function(err,data){ - if(err) { + this.app.put("/api/test_sound_volume", function (req, res) { + self.vacuum.testSoundVolume(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -439,10 +416,10 @@ const WebServer = function(options) { }); }); - this.app.put("/api/wifi_configuration", function(req,res) { - if(req.body && req.body.ssid && req.body.password) { - self.vacuum.configureWifi(req.body.ssid, req.body.password, function(err,data) { - if(err) { + this.app.put("/api/wifi_configuration", function (req, res) { + if (req.body && req.body.ssid && req.body.password) { + self.vacuum.configureWifi(req.body.ssid, req.body.password, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -453,10 +430,10 @@ const WebServer = function(options) { } }); - this.app.put("/api/reset_consumable", function(req,res) { - if(req.body && typeof req.body.consumable === "string") { - self.vacuum.resetConsumable(req.body.consumable, function(err,data) { - if(err) { + this.app.put("/api/reset_consumable", function (req, res) { + if (req.body && typeof req.body.consumable === "string") { + self.vacuum.resetConsumable(req.body.consumable, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -467,9 +444,9 @@ const WebServer = function(options) { } }); - this.app.put("/api/find_robot", function(req,res){ - self.vacuum.findRobot(function(err,data){ - if(err) { + this.app.put("/api/find_robot", function (req, res) { + self.vacuum.findRobot(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -477,9 +454,9 @@ const WebServer = function(options) { }); }); - this.app.put("/api/drive_home", function(req,res){ - self.vacuum.driveHome(function(err,data){ - if(err) { + this.app.put("/api/drive_home", function (req, res) { + self.vacuum.driveHome(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -487,18 +464,18 @@ const WebServer = function(options) { }); }); - this.app.put("/api/spot_clean", function(req,res){ - self.vacuum.spotClean(function(err,data){ - if(err) { + this.app.put("/api/spot_clean", function (req, res) { + self.vacuum.spotClean(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); } }); }); - this.app.put("/api/start_manual_control", function(req,res){ - self.vacuum.startManualControl(function(err,data){ - if(err) { + this.app.put("/api/start_manual_control", function (req, res) { + self.vacuum.startManualControl(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -506,9 +483,9 @@ const WebServer = function(options) { }); }); - this.app.put("/api/stop_manual_control", function(req,res){ - self.vacuum.stopManualControl(function(err,data){ - if(err) { + this.app.put("/api/stop_manual_control", function (req, res) { + self.vacuum.stopManualControl(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -516,11 +493,11 @@ const WebServer = function(options) { }); }); - this.app.put("/api/set_manual_control", function(req,res){ - if(req.body && req.body.angle !== undefined && req.body.velocity !== undefined + this.app.put("/api/set_manual_control", function (req, res) { + if (req.body && req.body.angle !== undefined && req.body.velocity !== undefined && req.body.duration !== undefined && req.body.sequenceId !== undefined) { - self.vacuum.setManualControl(req.body.angle, req.body.velocity, req.body.duration, req.body.sequenceId, function(err,data){ - if(err) { + self.vacuum.setManualControl(req.body.angle, req.body.velocity, req.body.duration, req.body.sequenceId, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -529,21 +506,21 @@ const WebServer = function(options) { } }); - this.app.get("/api/get_carpet_mode", function(req,res){ - self.vacuum.getCarpetMode(function(err,data){ - if(err) { + this.app.get("/api/get_carpet_mode", function (req, res) { + self.vacuum.getCarpetMode(function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); } - }); + }); }); - this.app.put("/api/set_carpet_mode", function(req,res){ - if(req.body && req.body.enable !== undefined && req.body.current_integral !== undefined && req.body.current_low !== undefined - && req.body.current_high !== undefined && req.body.stall_time !== undefined) { - self.vacuum.setCarpetMode(req.body.enable, req.body.current_integral, req.body.current_low, req.body.current_high, req.body.stall_time, function(err,data){ - if(err) { + this.app.put("/api/set_carpet_mode", function (req, res) { + if (req.body && req.body.enable !== undefined && req.body.current_integral !== undefined && req.body.current_low !== undefined + && req.body.current_high !== undefined && req.body.stall_time !== undefined) { + self.vacuum.setCarpetMode(req.body.enable, req.body.current_integral, req.body.current_low, req.body.current_high, req.body.stall_time, function (err, data) { + if (err) { res.status(500).send(err.toString()); } else { res.json(data); @@ -552,19 +529,21 @@ const WebServer = function(options) { } }); - this.app.get("/api/remote/map", function(req,res){ - WebServer.FIND_LATEST_MAP(function(err, data){ - if(!err && data.mapData.map.length > 0) { - var width=1024; - var height=width; + this.app.get("/api/remote/map", function (req, res) { + const self = this; + + WebServer.FIND_LATEST_MAP(function (err, data) { + if (!err && data.mapData.map.length > 0) { + var width = 1024; + var height = 1024; //create current map - new Jimp(width, height, function(err, image) { - if(!err) { + new Jimp(width, height, function (err, image) { + if (!err) { //configuration //default parameter - var scale=4; - var doCropping=true; - var border=2; + var scale = 4; + var doCropping = true; + var border = 2; var drawPath = true; var drawCharger = true; var drawRobot = true; @@ -572,10 +551,10 @@ const WebServer = function(options) { //get given parameter var urlObj = url.parse(req.url, true); if (urlObj['query']['scale'] !== undefined) { - scale = parseInt(urlObj['query']['scale'] ); + scale = parseInt(urlObj['query']['scale']); } if (urlObj['query']['border'] !== undefined) { - border = parseInt(urlObj['query']['border'] ); + border = parseInt(urlObj['query']['border']); } if (urlObj['query']['drawPath'] !== undefined) { drawPath = (urlObj['query']['drawPath'] == 'true'); @@ -594,43 +573,41 @@ const WebServer = function(options) { returnImage = (urlObj['query']['returnImage'] == 'true'); } //for cropping - var xMin=width; - var xMax=0; - var yMin=height; - var yMax=0; + var xMin = width; + var xMax = 0; + var yMin = height; + var yMax = 0; //variables for colors let color; - let colorFree=Jimp.rgbaToInt(0,118,255,255); - let colorObstacleStrong=Jimp.rgbaToInt(102,153,255,255); - let colorObstacleWeak=Jimp.rgbaToInt(82,174,255,255); + let colorFree = Jimp.rgbaToInt(0, 118, 255, 255); + let colorObstacleStrong = Jimp.rgbaToInt(102, 153, 255, 255); + let colorObstacleWeak = Jimp.rgbaToInt(82, 174, 255, 255); data.mapData.map.forEach(function (px) { //calculate positions of pixel number - var yPos = Math.floor(px[0] / (height*4)); - var xPos = ((px[0] - yPos*(width*4))/4); + var yPos = Math.floor(px[0] / (height * 4)); + var xPos = ((px[0] - yPos * (width * 4)) / 4); //cropping - if (yPos>yMax) yMax=yPos; - else if (yPosxMax) xMax=xPos; - else if (xPos yMax) yMax = yPos; + else if (yPos < yMin) yMin = yPos; + if (xPos > xMax) xMax = xPos; + else if (xPos < xMin) xMin = xPos; if (px[1] === 0 && px[2] === 0 && px[3] === 0) { - color=colorObstacleStrong; - } - else if (px[1] === 255 && px[2] === 255 && px[3] === 255) { - color=colorFree; - } - else { - color=colorObstacleWeak; + color = colorObstacleStrong; + } else if (px[1] === 255 && px[2] === 255 && px[3] === 255) { + color = colorFree; + } else { + color = colorObstacleWeak; } //set pixel on position - image.setPixelColor(color, xPos, yPos ); + image.setPixelColor(color, xPos, yPos); }); //crop to the map content let croppingOffsetX = 0; let croppingOffsetY = 0; if (doCropping) { - croppingOffsetX = (xMin-border); - croppingOffsetY = (yMin-border); - image.crop( croppingOffsetX, croppingOffsetY, xMax-xMin+2*border, yMax-yMin+2*border ); + croppingOffsetX = (xMin - border); + croppingOffsetY = (yMin - border); + image.crop(croppingOffsetX, croppingOffsetY, xMax - xMin + 2 * border, yMax - yMin + 2 * border); } //scale the map image.scale(scale, Jimp.RESIZE_NEAREST_NEIGHBOR); @@ -642,32 +619,33 @@ const WebServer = function(options) { var startLine = 0; if (!drawPath && lines.length > 10) { //reduce unnecessarycalculation time if path is not drawn - startLine = lines.length-10; + startLine = lines.length - 10; } for (var lc = startLine, len = lines.length; lc < len; lc++) { line = lines[lc]; - if(line.indexOf("reset") !== -1) { + if (line.indexOf("reset") !== -1) { coords = []; } - if(line.indexOf("estimate") !== -1) { + if (line.indexOf("estimate") !== -1) { let splitLine = line.split(" "); - let x = (width/2) + (splitLine[2] * 20) ; + let x = (width / 2) + (splitLine[2] * 20); let y = splitLine[3] * 20; - if(data.mapData.yFlipped) { - y = y*-1; + if (data.mapData.yFlipped) { + y = y * -1; } //move coordinates to match cropped pane x -= croppingOffsetX; y -= croppingOffsetY; coords.push([ - Math.round(x*scale), - Math.round(((width/2)+y)*scale) + Math.round(x * scale), + Math.round(((width / 2) + y) * scale) ]); } - }; + } + //2. draw path let first = true; - let pathColor = Jimp.rgbaToInt(255,255,255,255); + let pathColor = Jimp.rgbaToInt(255, 255, 255, 255); let oldPathX, oldPathY; // old Coordinates let dx, dy; //delta x and y let step, x, y, i; @@ -675,10 +653,9 @@ const WebServer = function(options) { if (!first && drawPath) { dx = (coord[0] - oldPathX); dy = (coord[1] - oldPathY); - if(Math.abs(dx) >= Math.abs(dy)) { + if (Math.abs(dx) >= Math.abs(dy)) { step = Math.abs(dx); - } - else { + } else { step = Math.abs(dy); } dx = dx / step; @@ -686,7 +663,7 @@ const WebServer = function(options) { x = oldPathX; y = oldPathY; i = 1; - while(i <= step) { + while (i <= step) { image.setPixelColor(pathColor, x, y); x = x + dx; y = y + dy; @@ -702,40 +679,44 @@ const WebServer = function(options) { var robotAngle = 0; if (coords.length > 2) { //the image has the offset of 90 degrees (top = 0 deg) - robotAngle =90 + Math.atan2(coords[coords.length - 1][1] - coords[coords.length - 2][1], coords[coords.length - 1][0] - coords[coords.length - 2][0]) * 180 / Math.PI; + robotAngle = 90 + Math.atan2(coords[coords.length - 1][1] - coords[coords.length - 2][1], coords[coords.length - 1][0] - coords[coords.length - 2][0]) * 180 / Math.PI; } //use process.env.VAC_TMP_PATH to define a path on your dev machine - like C:/Windows/Temp var tmpDir = (process.env.VAC_TMP_PATH ? process.env.VAC_TMP_PATH : "/tmp"); var directory = "/maps/"; - if (!fs.existsSync(tmpDir + directory)){ - fs.mkdirSync(tmpDir + directory); + if (!fs.existsSync(tmpDir + directory)) { + fs.mkdirSync(tmpDir + directory); } //delete old image (keep the last 5 images generated) - var numerOfFiles =0; + var numerOfFiles = 0; fs.readdirSync(tmpDir + directory) - .sort(function(a, b) {return a < b;}) - .forEach(function (file) { - numerOfFiles++; - if (numerOfFiles>5) { - fs.unlink(tmpDir + directory + file, err => { if (err) console.log(err) }); - //console.log( "removing " + toClientDir + directory + file); - } - }); + .sort(function (a, b) { + return a < b; + }) + .forEach(function (file) { + numerOfFiles++; + if (numerOfFiles > 5) { + fs.unlink(tmpDir + directory + file, err => { + if (err) console.log(err) + }); + //console.log( "removing " + toClientDir + directory + file); + } + }); //Position on the bitmap (0/0 is bottom-left) - var homeX=0; - var homeY=0; + var homeX = 0; + var homeY = 0; var imagePath; //set charger position homeX = ((width / 2) - croppingOffsetX) * scale; - homeY = ((height / 2) - croppingOffsetY) * scale; + homeY = ((height / 2) - croppingOffsetY) * scale; //save image var date = new Date(); var dd = (date.getDate() < 10 ? '0' : '') + date.getDate(); var mm = ((date.getMonth() + 1) < 10 ? '0' : '') + (date.getMonth() + 1); var yyyy = date.getFullYear(); - var HH = (date.getHours()<10?'0':'') + date.getHours(); - var MM = (date.getMinutes()<10?'0':'') + date.getMinutes(); - var SS = (date.getSeconds()<10?'0':'') + date.getSeconds(); + var HH = (date.getHours() < 10 ? '0' : '') + date.getHours(); + var MM = (date.getMinutes() < 10 ? '0' : '') + date.getMinutes(); + var SS = (date.getSeconds() < 10 ? '0' : '') + date.getSeconds(); var fileName = yyyy + "-" + mm + "-" + dd + "_" + HH + "-" + MM + "-" + SS + ".png"; imagePath = directory + fileName; //Pretty dumb case selection (doubled code for charger drawing), but no idea how to get this implemented in a more clever way. @@ -750,25 +731,26 @@ const WebServer = function(options) { res.status(500).send(err.toString()); } else { //specify response content - res.writeHead(200,{'Content-type':'image/png'}); + res.writeHead(200, {'Content-type': 'image/png'}); res.end(content); } }); } else { res.json({ scale, - border : border*scale, + border: border * scale, doCropping, drawPath, - mapsrc : imagePath, + mapsrc: imagePath, drawCharger, - charger : [homeX, homeY], + charger: [homeX, homeY], drawRobot, - robot : [robotPositionX, robotPositionY], - robotAngle : Math.round(robotAngle) + robot: [robotPositionX, robotPositionY], + robotAngle: Math.round(robotAngle) }); } } + if (!drawCharger && !drawRobot) { //console.log("Drawing no charger - no robot!"); image.write(tmpDir + imagePath); @@ -777,15 +759,15 @@ const WebServer = function(options) { //robot should be drawn (and maybe charger) Jimp.read(robotImagePath) .then(robotImage => { - let xPos = robotPositionX - robotImage.bitmap.width/2; - let yPos = robotPositionY - robotImage.bitmap.height/2; + let xPos = robotPositionX - robotImage.bitmap.width / 2; + let yPos = robotPositionY - robotImage.bitmap.height / 2; robotImage.rotate(-1 * robotAngle); //counter clock wise image.composite(robotImage, xPos, yPos); if (drawCharger) { Jimp.read(chargerImagePath) .then(chargerImage => { - let xPos = homeX - chargerImage.bitmap.width/2; - let yPos = homeY - chargerImage.bitmap.height/2; + let xPos = homeX - chargerImage.bitmap.width / 2; + let yPos = homeY - chargerImage.bitmap.height / 2; image.composite(chargerImage, xPos, yPos); //console.log("Drawing charger - robot!"); image.write(tmpDir + imagePath); @@ -801,8 +783,8 @@ const WebServer = function(options) { //draw charger but no robot Jimp.read(chargerImagePath) .then(chargerImage => { - let xPos = homeX - chargerImage.bitmap.width/2; - let yPos = homeY - chargerImage.bitmap.height/2; + let xPos = homeX - chargerImage.bitmap.width / 2; + let yPos = homeY - chargerImage.bitmap.height / 2; image.composite(chargerImage, xPos, yPos); //console.log("Drawing charger - no robot!"); image.write(tmpDir + imagePath); @@ -816,26 +798,29 @@ const WebServer = function(options) { } else { res.status(500).send(err != null ? err.toString() : "No usable map found, start cleaning and try again."); } - }); + }, + self.configuration.get("dontFlipGridMap"), //TODO: this whole dontFlipGridMap thing is just weird. Fix this mess + self.configuration.get("preferGridMap") //TODO: same for preferGridMap + ); }); - this.app.get("/api/map/latest", function(req,res){ + this.app.get("/api/map/latest", function (req, res) { var parsedUrl = url.parse(req.url, true); const doNotTransformPath = parsedUrl.query.doNotTransformPath !== undefined; - WebServer.FIND_LATEST_MAP(function(err, data){ - if(!err) { + WebServer.FIND_LATEST_MAP(function (err, data) { + if (!err) { const lines = data.log.split("\n"); let coords = []; - lines.forEach(function(line){ - if(line.indexOf("reset") !== -1) { + lines.forEach(function (line) { + if (line.indexOf("reset") !== -1) { coords = []; } - if(line.indexOf("estimate") !== -1) { + if (line.indexOf("estimate") !== -1) { let sl = line.split(" "); let lx = sl[2]; let ly = sl[3]; - if(doNotTransformPath) { + if (doNotTransformPath) { coords.push([lx, ly]); } else { coords.push(MapFunctions.logCoordToCanvasCoord([lx, ly], data.mapData.yFlipped)); @@ -853,7 +838,7 @@ const WebServer = function(options) { }); }); - this.app.get("/api/token", function(req,res){ + this.app.get("/api/token", function (req, res) { res.json({ token: self.vacuum.token.toString("hex") }); @@ -861,14 +846,14 @@ const WebServer = function(options) { //this results in searching client folder first and //if file was not found within that folder, the tmp folder will be searched for that file - this.app.use(express.static(path.join(__dirname, "..", 'client'))); - this.app.use(express.static((process.env.VAC_TMP_PATH ? process.env.VAC_TMP_PATH : "/tmp"))); - this.app.listen(this.port, function(){ + this.app.use(express.static(path.join(__dirname, "../..", 'client'))); + this.app.use(express.static((process.env.VAC_TMP_PATH ? process.env.VAC_TMP_PATH : "/tmp"))); //TODO: Don't. No. This is bad. + this.app.listen(this.port, function () { console.log("Webserver running on port", self.port) }) }; -WebServer.PARSE_PPM_MAP = function(buf) { +WebServer.PARSE_PPM_MAP = function (buf) { const map = []; if (buf.length === WebServer.CORRECT_PPM_MAP_FILE_SIZE) { @@ -878,22 +863,22 @@ WebServer.PARSE_PPM_MAP = function(buf) { let b = buf.readUInt8(i + 2); if (!(r === 125 && g === 125 && b === 125)) { - map.push([j+j*3, r, g, b]) + map.push([j + j * 3, r, g, b]) } } } - return { map: map, yFlipped: true }; + return {map: map, yFlipped: true}; }; -WebServer.PARSE_GRID_MAP = function(buf) { +WebServer.PARSE_GRID_MAP = function (buf, dontFlipGridMap) { const map = []; - if(buf.length = WebServer.CORRECT_GRID_MAP_FILE_SIZE) { + if (buf.length === WebServer.CORRECT_GRID_MAP_FILE_SIZE) { for (let i = 0; i < buf.length; i++) { let px = buf.readUInt8(i); - if(px !== 0) { + if (px !== 0) { px = px === 1 ? 0 : px; map.push([i + i * 3, px, px, px]) } @@ -901,7 +886,7 @@ WebServer.PARSE_GRID_MAP = function(buf) { } // y will be flipped by default, unless dontFlipGridMap is set in the configuration. - let yFlipped = !WebServer.configuration.dontFlipGridMap; + let yFlipped = !dontFlipGridMap; if (yFlipped) { let width = 1024, height = 1024, size = 4; let transform = MapFunctions.TRANSFORM_COORD_FLIP_Y; @@ -912,17 +897,19 @@ WebServer.PARSE_GRID_MAP = function(buf) { map[i][0] = MapFunctions.mapCoordToMapIndex(xy2, width, height, size); } } - return { map: map, yFlipped: yFlipped }; + return {map: map, yFlipped: yFlipped}; }; -WebServer.PARSE_MAP_AUTO = function(filename) { + +//TODO: why is nothing using this??? +WebServer.PARSE_MAP_AUTO = function (filename, dontFlipGridMap) { // this function automatically determines whether a GridMap or PPM map is used, based on file size. - let mapData = { map: [], yFlipped: true }; + let mapData = {map: [], yFlipped: true}; try { const mapBytes = fs.readFileSync(filename); if (mapBytes.length === WebServer.CORRECT_GRID_MAP_FILE_SIZE) { - mapData = WebServer.PARSE_GRID_MAP(mapBytes); + mapData = WebServer.PARSE_GRID_MAP(mapBytes, dontFlipGridMap); } else if (mapBytes.length === WebServer.CORRECT_PPM_MAP_FILE_SIZE) { mapData = WebServer.PARSE_PPM_MAP(mapBytes); } @@ -933,15 +920,15 @@ WebServer.PARSE_MAP_AUTO = function(filename) { return mapData; }; -WebServer.GENERATE_TEST_MAP = function() { +WebServer.GENERATE_TEST_MAP = function () { let mapData = []; - for(let y = 0; y < 1024; y++) { - for(let x = 0; x < 1024; x++) { + for (let y = 0; y < 1024; y++) { + for (let x = 0; x < 1024; x++) { let index = 4 * (y * 1024 + x); // 4x4m square - if(x >= 472 && x <= 552 && y >= 472 && y <= 552) { - if(x == 472 || x == 552 || y == 472 || y == 552) { + if (x >= 472 && x <= 552 && y >= 472 && y <= 552) { + if (x === 472 || x === 552 || y === 472 || y === 552) { mapData.push([index, 0, 0, 0]); } else { mapData.push([index, 255, 255, 255]); @@ -950,9 +937,12 @@ WebServer.GENERATE_TEST_MAP = function() { } } return {map: mapData, yFlipped: false}; -} +}; -WebServer.GENERATE_TEST_PATH = function() { +/** + * @return {string} + */ +WebServer.GENERATE_TEST_PATH = function () { let lines = [ // 3 "estimate 0 -1.5 -0.5", @@ -986,52 +976,52 @@ WebServer.GENERATE_TEST_PATH = function() { "estimate 0 0.75 0.5" ]; return lines.join("\n"); -} +}; -WebServer.FIND_LATEST_MAP = function(callback) { - if(process.env.VAC_MAP_TEST) { +WebServer.FIND_LATEST_MAP = function (callback, dontFlipGridMap, preferGridMap) { + if (process.env.VAC_MAP_TEST) { callback(null, { mapData: WebServer.GENERATE_TEST_MAP(), log: WebServer.GENERATE_TEST_PATH() }) } else { - WebServer.FIND_LATEST_MAP_IN_RAMDISK(callback); + WebServer.FIND_LATEST_MAP_IN_RAMDISK(callback, dontFlipGridMap, preferGridMap); } }; -WebServer.FIND_LATEST_MAP_IN_RAMDISK = function(callback) { - fs.readdir("/dev/shm", function(err, filenames){ - if(err) { +WebServer.FIND_LATEST_MAP_IN_RAMDISK = function (callback, dontFlipGridMap, preferGridMap) { + fs.readdir("/dev/shm", function (err, filenames) { + if (err) { callback(err); } else { let mapFileName; let logFileName; - filenames.forEach(function(filename){ - if(filename.endsWith(".ppm")) { + filenames.forEach(function (filename) { + if (filename.endsWith(".ppm")) { mapFileName = filename; } - if(filename === "SLAM_fprintf.log") { + if (filename === "SLAM_fprintf.log") { logFileName = filename; } }); - if(mapFileName && logFileName) { - fs.readFile(path.join("/dev/shm", logFileName), function(err, file){ - if(err) { + if (mapFileName && logFileName) { + fs.readFile(path.join("/dev/shm", logFileName), function (err, file) { + if (err) { callback(err); } else { const log = file.toString(); - if(log.indexOf("estimate") !== -1) { + if (log.indexOf("estimate") !== -1) { - let loadGridMap = function() { + let loadGridMap = function () { fs.readFile("/dev/shm/GridMap", function (err, gridMapFile) { if (err) { callback(new Error("Unable to get complete map file")) } else { callback(null, { - mapData: WebServer.PARSE_GRID_MAP(gridMapFile), + mapData: WebServer.PARSE_GRID_MAP(gridMapFile, dontFlipGridMap), log: log }) } @@ -1040,7 +1030,7 @@ WebServer.FIND_LATEST_MAP_IN_RAMDISK = function(callback) { // if the user knows that there will only ever be usable gridmaps, // setting the configuration option "preferGridMap" will take a shortcut. - if (WebServer.configuration.preferGridMap) { + if (preferGridMap) { loadGridMap(); } else { let mapPath = path.join("/dev/shm", mapFileName); @@ -1090,15 +1080,15 @@ WebServer.FIND_LATEST_MAP_IN_RAMDISK = function(callback) { }) }; -WebServer.FIND_LATEST_MAP_IN_ARCHIVE = function(callback) { - fs.readdir("/mnt/data/rockrobo/rrlog", function(err, filenames){ - if(err) { +WebServer.FIND_LATEST_MAP_IN_ARCHIVE = function (callback) { + fs.readdir("/mnt/data/rockrobo/rrlog", function (err, filenames) { + if (err) { callback(err); } else { let folders = []; - filenames.forEach(function(filename){ - if(/^([0-9]{6})\.([0-9]{17})_(R([0-9]{4})S([0-9]{8})|[0-9]{13})_([0-9]{10})REL$/.test(filename)) { + filenames.forEach(function (filename) { + if (/^([0-9]{6})\.([0-9]{17})_(R([0-9]{4})S([0-9]{8})|[0-9]{13})_([0-9]{10})REL$/.test(filename)) { folders.push(filename); } }); @@ -1108,7 +1098,7 @@ WebServer.FIND_LATEST_MAP_IN_ARCHIVE = function(callback) { let mapFileName; let logFileName; - for(let i in folders) { + for (let i in folders) { const folder = folders[i]; try { const folderContents = fs.readdirSync(path.join("/mnt/data/rockrobo/rrlog", folder)); @@ -1117,11 +1107,11 @@ WebServer.FIND_LATEST_MAP_IN_ARCHIVE = function(callback) { logFileName = undefined; - folderContents.forEach(function(filename){ - if(/^navmap([0-9]+)\.ppm\.([0-9]{4})(\.rr)?\.gz$/.test(filename)) { + folderContents.forEach(function (filename) { + if (/^navmap([0-9]+)\.ppm\.([0-9]{4})(\.rr)?\.gz$/.test(filename)) { possibleMapFileNames.push(filename); } - if(/^SLAM_fprintf\.log\.([0-9]{4})(\.rr)?\.gz$/.test(filename)) { + if (/^SLAM_fprintf\.log\.([0-9]{4})(\.rr)?\.gz$/.test(filename)) { logFileName = filename; } }); @@ -1129,7 +1119,7 @@ WebServer.FIND_LATEST_MAP_IN_ARCHIVE = function(callback) { possibleMapFileNames = possibleMapFileNames.sort(); mapFileName = possibleMapFileNames.pop(); - if(mapFileName && logFileName) { + if (mapFileName && logFileName) { newestUsableFolderName = folder; break; } @@ -1138,23 +1128,23 @@ WebServer.FIND_LATEST_MAP_IN_ARCHIVE = function(callback) { } } - if(newestUsableFolderName && mapFileName && logFileName) { - fs.readFile(path.join("/mnt/data/rockrobo/rrlog", newestUsableFolderName, logFileName), function(err, file){ - if(err) { + if (newestUsableFolderName && mapFileName && logFileName) { + fs.readFile(path.join("/mnt/data/rockrobo/rrlog", newestUsableFolderName, logFileName), function (err, file) { + if (err) { callback(err); } else { - WebServer.DECRYPT_AND_UNPACK_FILE(file, function(err, unzippedFile){ - if(err) { + WebServer.DECRYPT_AND_UNPACK_FILE(file, function (err, unzippedFile) { + if (err) { callback(err); } else { const log = unzippedFile.toString(); - if(log.indexOf("estimate") !== -1) { - fs.readFile(path.join("/mnt/data/rockrobo/rrlog", newestUsableFolderName, mapFileName), function(err, file){ - if(err) { + if (log.indexOf("estimate") !== -1) { + fs.readFile(path.join("/mnt/data/rockrobo/rrlog", newestUsableFolderName, mapFileName), function (err, file) { + if (err) { callback(err); } else { - WebServer.DECRYPT_AND_UNPACK_FILE(file, function(err, unzippedFile){ - if(err) { + WebServer.DECRYPT_AND_UNPACK_FILE(file, function (err, unzippedFile) { + if (err) { callback(err); } else { callback(null, { @@ -1179,21 +1169,21 @@ WebServer.FIND_LATEST_MAP_IN_ARCHIVE = function(callback) { }) }; -WebServer.DECRYPT_AND_UNPACK_FILE = function(file, callback) { +WebServer.DECRYPT_AND_UNPACK_FILE = function (file, callback) { const decipher = crypto.createDecipheriv("aes-128-ecb", WebServer.ENCRYPTED_ARCHIVE_DATA_PASSWORD, ""); let decryptedBuffer; - if(Buffer.isBuffer(file)) { + if (Buffer.isBuffer(file)) { //gzip magic bytes - if(WebServer.BUFFER_IS_GZIP(file)) { + if (WebServer.BUFFER_IS_GZIP(file)) { zlib.gunzip(file, callback); } else { try { decryptedBuffer = Buffer.concat([decipher.update(file), decipher.final()]); - } catch(e) { + } catch (e) { return callback(e); } - if(WebServer.BUFFER_IS_GZIP(decryptedBuffer)) { + if (WebServer.BUFFER_IS_GZIP(decryptedBuffer)) { zlib.gunzip(decryptedBuffer, callback); } else { callback(new Error("Couldn't decrypt file")); @@ -1204,22 +1194,11 @@ WebServer.DECRYPT_AND_UNPACK_FILE = function(file, callback) { } }; -WebServer.BUFFER_IS_GZIP = function(buf) { +WebServer.BUFFER_IS_GZIP = function (buf) { return Buffer.isBuffer(buf) && buf[0] === 0x1f && buf[1] === 0x8b; }; -WebServer.MK_DIR_PATH = function(filepath) { - var dirname = path.dirname(filepath); - if (!fs.existsSync(dirname)) { - WebServer.MK_DIR_PATH(dirname); - } - if (!fs.existsSync(filepath)) { - fs.mkdirSync(filepath); - } - -} - WebServer.CORRECT_PPM_MAP_FILE_SIZE = 3145745; WebServer.CORRECT_GRID_MAP_FILE_SIZE = 1048576; WebServer.ENCRYPTED_ARCHIVE_DATA_PASSWORD = Buffer.from("RoCKR0B0@BEIJING"); diff --git a/package.json b/package.json index 0d3f0405..5aaa4eca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "valetudo", - "version": "0.2.0", + "version": "0.2.1", "description": "Self-contained control webinterface for xiaomi vacuum robots", "main": "index.js", "bin": "index.js", @@ -12,7 +12,7 @@ }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build": "pkg --targets node10-linux-armv7 --no-bytecode --options max-old-space-size=72 --public-packages=exif-parser,omggif,trim,prettycron ." + "build": "pkg --targets node10-linux-armv7 --no-bytecode --options max-old-space-size=72 --public-packages=exif-parser,omggif,trim,prettycron,mqtt ." }, "author": "", "dependencies": { @@ -20,6 +20,7 @@ "compression": "^1.7.2", "express": "^4.16.3", "jimp": "0.3.2", + "mqtt": "^2.18.8", "prettycron": "^0.10.0" }, "devDependencies": {