├── .gitignore ├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── packages ├── release └── versions ├── LICENSE ├── README.md ├── client ├── client.js ├── mqtt.css └── mqtt.html ├── mqtt.js ├── packages.json ├── packages └── npm │ ├── .gitignore │ ├── .npm │ └── package │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── README │ │ └── npm-shrinkwrap.json │ ├── index.js │ ├── package.js │ ├── test.js │ └── versions.json └── server ├── config.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 7qk64mfblh5z9nbhm1 8 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | standard-app-packages 7 | insecure 8 | npm 9 | pinglamb:bootstrap3 10 | 11 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@0.9.1 2 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | application-configuration@1.0.1 2 | autoupdate@1.0.6 3 | binary-heap@1.0.0 4 | blaze-tools@1.0.0 5 | blaze@2.0.0 6 | callback-hook@1.0.0 7 | check@1.0.0 8 | ctl-helper@1.0.3 9 | ctl@1.0.1 10 | ddp@1.0.8 11 | ejson@1.0.1 12 | follower-livedata@1.0.1 13 | geojson-utils@1.0.0 14 | html-tools@1.0.0 15 | htmljs@1.0.0 16 | id-map@1.0.0 17 | insecure@1.0.0 18 | jquery@1.0.0 19 | json@1.0.0 20 | logging@1.0.2 21 | meteor-platform@1.0.1 22 | meteor@1.0.3 23 | minifiers@1.0.2 24 | minimongo@1.0.2 25 | mongo@1.0.4 26 | npm@0.0.0 27 | observe-sequence@1.0.2 28 | ordered-dict@1.0.0 29 | pinglamb:bootstrap3@3.2.1 30 | random@1.0.0 31 | reactive-dict@1.0.1 32 | reactive-var@1.0.1 33 | reload@1.0.1 34 | retry@1.0.0 35 | routepolicy@1.0.0 36 | session@1.0.1 37 | spacebars-compiler@1.0.2 38 | spacebars@1.0.1 39 | standard-app-packages@1.0.1 40 | templating@1.0.5 41 | tracker@1.0.2 42 | underscore@1.0.0 43 | webapp@1.0.3 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Niko Köbler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Meteor MQTT Client 2 | ================== 3 | 4 | [Meteor](http://www.meteor.com) [MQTT](http://mqtt.org/) client using [mqtt package from NPM](https://www.npmjs.org/package/mqtt). 5 | -------------------------------------------------------------------------------- /client/client.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // subscribe to the published collection 4 | Meteor.subscribe("mqttMessages"); 5 | 6 | // we need a dependency later on to refresh the topic query 7 | var topicDep = new Deps.Dependency; 8 | 9 | Session.setDefault("configValues", {}); 10 | 11 | // this is the dependend function to retrieve and set the topic query 12 | Deps.autorun(function(){ 13 | topicDep.depend(); 14 | Meteor.call("getTopicQuery", function(err, obj) { 15 | Session.set("topicQuery", obj); 16 | }); 17 | }); 18 | 19 | Meteor.startup(function() { 20 | Meteor.call("getConfigValues", function(err, values) { 21 | Session.set("configValues", values); 22 | }); 23 | }); 24 | 25 | Template.config.host = function() { 26 | return Session.get("configValues")["mqttHost"]; 27 | }; 28 | 29 | Template.config.port = function() { 30 | return Session.get("configValues")["mqttPort"]; 31 | }; 32 | 33 | Template.messages.topicQuery = function() { 34 | return Session.get("topicQuery"); 35 | }; 36 | 37 | Template.messages.lastMessages = function () { 38 | return Messages.find({}, {sort: {ts: -1}}); 39 | }; 40 | 41 | // just for a better readability in the UI 42 | Template.msg.tsString = function() { 43 | return this.ts.toLocaleString(); 44 | }; 45 | 46 | // the start/stop button events, call the server-side methods 47 | Template.admin.events({ 48 | 'click #startClient': function() { 49 | Meteor.call("startClient"); 50 | }, 51 | 'click #stopClient': function() { 52 | Meteor.call("stopClient"); 53 | } 54 | }); 55 | 56 | // the events for changing the topic query (for button click and pressing enter) 57 | Template.topic.events({ 58 | 'click #sendTopicQuery': function() { 59 | _sendTopic(); 60 | }, 61 | 'keyup #topicQuery': function(e) { 62 | if (e.type == "keyup" && e.which == 13) { 63 | _sendTopic(); 64 | } 65 | } 66 | }); 67 | 68 | // the click-event for publishing a message 69 | Template.publish.events({ 70 | 'click #publishMessage': function() { 71 | var elTopic = document.getElementById("topic"); 72 | var elMessage = document.getElementById("message"); 73 | Meteor.call("publishMessage", elTopic.value, elMessage.value, function() { 74 | elTopic.value = ""; 75 | elMessage.value = ""; 76 | elTopic.focus(); 77 | }); 78 | } 79 | }); 80 | 81 | // get the new query from the input field and send it to the server, reset field 82 | // tell the dependency, that it has changed and has to be run again 83 | var _sendTopic = function() { 84 | var el = document.getElementById("topicQuery"); 85 | var topicQuery = el.value; 86 | Meteor.call("setTopicQuery", topicQuery, function() { 87 | topicDep.changed(); 88 | el.value = ""; 89 | el.focus(); 90 | }); 91 | }; -------------------------------------------------------------------------------- /client/mqtt.css: -------------------------------------------------------------------------------- 1 | /* CSS declarations go here */ 2 | body { 3 | margin: 10px; 4 | } -------------------------------------------------------------------------------- /client/mqtt.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Meteor MQTT Dashboard 4 | 5 | 6 | 7 |
8 |

Meteor MQTT Dashboard

9 | {{> config}} 10 | {{> admin}} 11 |
12 |
13 | {{> topic}} 14 |
15 |
16 | {{> publish}} 17 |
18 |
19 | {{> messages}} 20 |
21 | 22 | 23 | 29 | 30 | 33 | 34 | 39 | 40 | 57 | 58 | 65 | 66 | -------------------------------------------------------------------------------- /mqtt.js: -------------------------------------------------------------------------------- 1 | // initializing the collection, used from client and server 2 | Messages = new Meteor.Collection("mqtt-messages"); 3 | -------------------------------------------------------------------------------- /packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "mqtt": "0.3.11" 3 | } -------------------------------------------------------------------------------- /packages/npm/.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /packages/npm/.npm/package/.gitattributes: -------------------------------------------------------------------------------- 1 | /npm-shrinkwrap.json eol=lf 2 | /README eol=lf 3 | /.git* eol=lf 4 | -------------------------------------------------------------------------------- /packages/npm/.npm/package/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/npm/.npm/package/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /packages/npm/.npm/package/npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mqtt": { 4 | "version": "0.3.11", 5 | "dependencies": { 6 | "readable-stream": { 7 | "version": "1.0.27-1", 8 | "dependencies": { 9 | "core-util-is": { 10 | "version": "1.0.1" 11 | }, 12 | "isarray": { 13 | "version": "0.0.1" 14 | }, 15 | "string_decoder": { 16 | "version": "0.10.25-1" 17 | }, 18 | "inherits": { 19 | "version": "2.0.1" 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/npm/index.js: -------------------------------------------------------------------------------- 1 | var Future = Npm.require('fibers/future'); 2 | Async = {}; 3 | 4 | Meteor.require = function(moduleName) { 5 | var module = Npm.require(moduleName); 6 | return module; 7 | }; 8 | 9 | Async.runSync = Meteor.sync = function(asynFunction) { 10 | var future = new Future(); 11 | var sent = false; 12 | var payload; 13 | 14 | var wrappedAsyncFunction = Meteor.bindEnvironment(asynFunction, function(err) { 15 | console.error('Error inside the Async.runSync: ' + err.message); 16 | returnFuture(err); 17 | }); 18 | 19 | setTimeout(function() { 20 | wrappedAsyncFunction(returnFuture); 21 | }, 0); 22 | 23 | future.wait(); 24 | sent = true; 25 | 26 | function returnFuture(error, result) { 27 | if(!sent) { 28 | payload = { result: result, error: error}; 29 | future.return(); 30 | } 31 | } 32 | 33 | return payload; 34 | }; 35 | 36 | Async.wrap = function(arg1, arg2) { 37 | if(typeof arg1 == 'function') { 38 | var func = arg1; 39 | return wrapFunction(func); 40 | } else if(typeof arg1 == 'object' && typeof arg2 == 'string') { 41 | var obj = arg1; 42 | var funcName = arg2; 43 | return wrapObject(obj, [funcName])[funcName]; 44 | } else if(typeof arg1 == 'object' && arg2 instanceof Array) { 45 | var obj = arg1; 46 | var funcNameList = arg2; 47 | return wrapObject(obj, funcNameList); 48 | } else { 49 | throw new Error('unsupported argument list'); 50 | } 51 | 52 | function wrapObject(obj, funcNameList) { 53 | var returnObj = {}; 54 | funcNameList.forEach(function(funcName) { 55 | if(obj[funcName]) { 56 | var func = obj[funcName].bind(obj); 57 | returnObj[funcName] = wrapFunction(func); 58 | } else { 59 | throw new Error('instance method not exists: ' + funcName); 60 | } 61 | }); 62 | return returnObj; 63 | } 64 | 65 | function wrapFunction(func) { 66 | return function() { 67 | var args = arguments; 68 | response = Meteor.sync(function(done) { 69 | Array.prototype.push.call(args, done); 70 | func.apply(null, args); 71 | }); 72 | 73 | if(response.error) { 74 | //we need to wrap a new error here something throw error object comes with response does not 75 | //print the correct error to the console, if there is not try catch block 76 | var error = new Error(response.error.message); 77 | for(var key in response.error) { 78 | if(error[key] === undefined) { 79 | error[key] = response.error[key]; 80 | } 81 | } 82 | throw error; 83 | } else { 84 | return response.result; 85 | } 86 | }; 87 | } 88 | }; -------------------------------------------------------------------------------- /packages/npm/package.js: -------------------------------------------------------------------------------- 1 | var path = Npm.require('path'); 2 | var fs = Npm.require('fs'); 3 | var packagesJsonFile = path.resolve('./packages.json'); 4 | 5 | //creating `packages.json` file for the first-time if not exists 6 | if(!fs.existsSync(packagesJsonFile)) { 7 | fs.writeFileSync(packagesJsonFile, '{\n \n}') 8 | } 9 | 10 | try { 11 | var fileContent = fs.readFileSync(packagesJsonFile); 12 | var packages = JSON.parse(fileContent.toString()); 13 | Npm.depends(packages); 14 | } catch(ex) { 15 | console.error('ERROR: packages.json parsing error [ ' + ex.message + ' ]'); 16 | } 17 | 18 | Package.describe({ 19 | summary: "complete npm integration/support for Meteor" 20 | }); 21 | 22 | Package.on_use(function (api, where) { 23 | api.export('Async'); 24 | 25 | var packagesFile = './.meteor/packages'; 26 | if(fs.existsSync(packagesFile) && isNewerMeteor) { 27 | api.add_files(['index.js', '../../packages.json'], 'server'); 28 | } else { 29 | api.add_files(['index.js'], 'server'); 30 | } 31 | 32 | function isNewerMeteor() { 33 | return fs.readFileSync(packagesFile, 'utf8').match(/\nstandard-app-packages/); 34 | } 35 | }); 36 | 37 | Package.on_test(function (api) { 38 | api.use(['tinytest']); 39 | api.add_files(['index.js', 'test.js'], 'server'); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/npm/test.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('Async.runSync - with done()', function(test) { 2 | var output = Async.runSync(function(done) { 3 | setTimeout(function() { 4 | done(null, 10001); 5 | }, 10); 6 | }); 7 | 8 | test.equal(output.result, 10001); 9 | test.equal(output.error, null); 10 | }); 11 | 12 | Tinytest.add('Async.runSync - with error()', function(test) { 13 | var output = Async.runSync(function(done) { 14 | setTimeout(function() { 15 | done({message: 'error-message', code: 402}); 16 | }, 10); 17 | }); 18 | 19 | test.equal(output.result, undefined); 20 | test.equal(output.error.code, 402); 21 | }); 22 | 23 | Tinytest.add('Async.runSync - with error in the callback', function(test) { 24 | var output = Async.runSync(function(done) { 25 | throw new Error('SOME_ERROR'); 26 | }); 27 | 28 | test.equal(output.result, undefined); 29 | test.equal(output.error.message, 'SOME_ERROR'); 30 | }); 31 | 32 | Tinytest.add('Async.wrap function mode - success', function(test) { 33 | function wait(timeout, callback) { 34 | setTimeout(function() { 35 | callback(null, 'okay'); 36 | }, timeout); 37 | }; 38 | 39 | var enclosedWait = Async.wrap(wait); 40 | var output = enclosedWait(100); 41 | 42 | test.equal(output, 'okay'); 43 | }); 44 | 45 | Tinytest.add('Async.wrap function mode - error', function(test) { 46 | function wait(timeout, callback) { 47 | setTimeout(function() { 48 | var error = new Error('THE_ERROR'); 49 | error.code = 500; 50 | callback(error); 51 | }, timeout); 52 | }; 53 | 54 | var enclosedWait = Async.wrap(wait); 55 | try { 56 | enclosedWait(100); 57 | test.fail('there must be an error'); 58 | } catch(err) { 59 | test.ok(err.message.match('THE_ERROR')); 60 | test.equal(err.code, 500); 61 | } 62 | 63 | }); 64 | 65 | Tinytest.add('Async.wrap object mode - success', function(test) { 66 | function Wait() { 67 | this.start = function(timeout, callback) { 68 | setTimeout(function() { 69 | callback(null, 'okay'); 70 | }, timeout); 71 | }; 72 | } 73 | 74 | var wait = new Wait(); 75 | 76 | var enclosedWait = Async.wrap(wait, 'start'); 77 | 78 | var output = enclosedWait(100); 79 | test.equal(output, 'okay'); 80 | }); 81 | 82 | Tinytest.add('Async.wrap object mode - funcName not exists', function(test) { 83 | function Wait() { 84 | this.start = function(timeout, callback) { 85 | setTimeout(function() { 86 | callback(null, 'okay'); 87 | }, timeout); 88 | }; 89 | } 90 | 91 | var wait = new Wait(); 92 | try { 93 | var enclosedWait = Async.wrap(wait, 'startz'); 94 | test.fail('shoud throw an error'); 95 | } catch(ex) { 96 | 97 | } 98 | }); 99 | 100 | Tinytest.add('Async.wrap object mode - multi function mode', function(test) { 101 | function Wait() { 102 | this.start = function(timeout, callback) { 103 | setTimeout(function() { 104 | callback(null, 'okay'); 105 | }, timeout); 106 | }; 107 | 108 | this.start2 = function(timeout, callback) { 109 | setTimeout(function() { 110 | callback(null, 'okay'); 111 | }, timeout); 112 | }; 113 | } 114 | 115 | var wait = new Wait(); 116 | var enclosedWait = Async.wrap(wait, ['start', 'start2']); 117 | enclosedWait.start(100); 118 | enclosedWait.start2(100); 119 | }); -------------------------------------------------------------------------------- /packages/npm/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "meteor", 5 | "1.0.3" 6 | ], 7 | [ 8 | "underscore", 9 | "1.0.0" 10 | ] 11 | ], 12 | "pluginDependencies": [], 13 | "toolVersion": "meteor-tool@1.0.27", 14 | "format": "1.0" 15 | } -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | config = { 2 | mqttHost: "broker.mqttdashboard.com", 3 | mqttPort: 1883 4 | }; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // default topic query 4 | var topicQuery = "#"; 5 | 6 | // for development purposes, delete the DB on startup, don't collect too much old data 7 | Meteor.startup(function () { 8 | Messages.remove({}); 9 | }); 10 | 11 | // data has to be published, autopublish is turned off! 12 | // return only the last 10 messages to the client 13 | Meteor.publish("mqttMessages", function() { 14 | return Messages.find({}, {sort: {ts: -1}, limit: 10}); 15 | }); 16 | 17 | // initialize the mqtt client from mqtt npm-package 18 | var mqtt = Meteor.require("mqtt"); 19 | var mqttClient = mqtt.createClient(config.mqttPort, config.mqttHost); 20 | mqttClient 21 | .on("connect", function() { 22 | console.log("client connected"); 23 | }) 24 | .on("message", function(topic, message) { 25 | console.log(topic + ": " + message); 26 | // build the object to store 27 | var msg = { 28 | message: message, 29 | topic: topic, 30 | ts: new Date() 31 | }; 32 | // add the message to the collection (see below...) 33 | addMsgToCollection(msg); 34 | }); 35 | 36 | // function is called when message is received (see above) 37 | // to get access to Meteor resources from non-Meteor callbacks, this has to be bound in Meteor environment 38 | var addMsgToCollection = Meteor.bindEnvironment(function(message) { 39 | Messages.insert(message); 40 | }); 41 | 42 | // some methods called by the client 43 | Meteor.methods({ 44 | // start receiving messages with the set topic-query 45 | startClient: function() { 46 | console.log("startClient called"); 47 | mqttClient.subscribe(topicQuery); 48 | }, 49 | // stop receiving messages 50 | stopClient: function() { 51 | console.log("stopClient called"); 52 | mqttClient.unsubscribe(topicQuery); 53 | }, 54 | // set a new topic query, unsubscribe from the old and subscribe to the new one 55 | setTopicQuery: function(newTopicQuery) { 56 | console.log("set new Topic: " + newTopicQuery); 57 | mqttClient.unsubscribe(topicQuery).subscribe(newTopicQuery); 58 | topicQuery = newTopicQuery; 59 | }, 60 | // send the topic query to the caller 61 | getTopicQuery: function() { 62 | return topicQuery; 63 | }, 64 | // publishes a message with a topic to the broker 65 | publishMessage: function(topic, message) { 66 | console.log("message to send: " + topic + ": " + message); 67 | mqttClient.publish(topic, message, function() { 68 | console.log("message sent: " + message); 69 | }); 70 | }, 71 | getConfigValues: function() { 72 | return config; 73 | } 74 | }); 75 | 76 | // delete every 120 seconds old data (messages) from the collection/mongodb 77 | Meteor.setInterval(function() { 78 | Messages.remove({}); 79 | }, 2*60*1000); --------------------------------------------------------------------------------