├── .babelrc ├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .postcssrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app.js ├── bin └── www ├── build ├── build.js ├── check-versions.js ├── logo.png ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── app.js ├── dev.env.js ├── index.js ├── prod.env.js └── store.js ├── images ├── MQTT-Logo.png ├── screen_broker.png ├── screen_clients.png ├── screen_function.png ├── screen_maps.png ├── screen_values.png └── sketch_diagram.png ├── index.html ├── lib ├── MqttClient.js ├── broker.js ├── debug.js ├── jsonStore.js └── utils.js ├── package-lock.json ├── package.json ├── package.sh ├── pkg └── .gitkeep ├── public └── stylesheets │ └── style.css ├── routes ├── index.js └── users.js ├── src ├── App.vue ├── apis │ └── ConfigApis.js ├── assets │ ├── css │ │ └── customize.css │ └── logo.png ├── components │ ├── Broker.vue │ ├── Maps.vue │ ├── MqttClients.vue │ ├── Values.vue │ ├── custom │ │ └── file-input.vue │ └── dialogs │ │ ├── Map.vue │ │ ├── Mqtt_Client.vue │ │ ├── Settings.vue │ │ └── Value.vue ├── main.js ├── router │ └── index.js └── store │ ├── index.js │ └── mutations.js ├── static ├── .gitkeep ├── favicon.ico ├── favicons │ ├── android-chrome-192x192.png │ ├── android-chrome-384x384.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest └── logo.png ├── store └── .gitkeep └── views └── error.ejs /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: robertsLando 4 | patreon: user?u=16906849 5 | custom: paypal.me/daniellando 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | /store/ 5 | /pkg/ 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC. 4 | 5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 6 | 7 | #### [v1.2.5](https://github.com/robertsLando/Mqtt2Mqtt/compare/v1.2.4...v1.2.5) 8 | 9 | > 22 July 2020 10 | 11 | - fix: Update mqtt-nedb-store [`5132810`](https://github.com/robertsLando/Mqtt2Mqtt/commit/51328103219f7706def4587ba939b24803e55da3) 12 | - chore(deps): bump aedes@0.42.5 [`a55f362`](https://github.com/robertsLando/Mqtt2Mqtt/commit/a55f3621a873dfd316cfeb592246ba169fcdc311) 13 | - fix: mqtt client end throws error [`ef508ea`](https://github.com/robertsLando/Mqtt2Mqtt/commit/ef508ea7e613d33a6aed8f3ff095fb107ae15d3b) 14 | - chore: Fixed package script [`d18dee0`](https://github.com/robertsLando/Mqtt2Mqtt/commit/d18dee05d8e27b0bfa5d1b761c820f85b32573b9) 15 | - fix: Better mqtt clients close handling [`48feca0`](https://github.com/robertsLando/Mqtt2Mqtt/commit/48feca0266dd68bc462b99d5c9fadb03cf1513a5) 16 | - chore: Fixed pkg release script [`d487004`](https://github.com/robertsLando/Mqtt2Mqtt/commit/d48700408ab360769418bdbcb6c3da3253221297) 17 | - fix: autoCompact not working on both store [`741d273`](https://github.com/robertsLando/Mqtt2Mqtt/commit/741d2736f3dbf55121177d32b5c09078c0da3719) 18 | 19 | #### [v1.2.4](https://github.com/robertsLando/Mqtt2Mqtt/compare/v1.2.3...v1.2.4) 20 | 21 | > 21 January 2020 22 | 23 | - chore: Updated to aedes@0.40.1 [`7d0e0ff`](https://github.com/robertsLando/Mqtt2Mqtt/commit/7d0e0ff873a891d34900f362204a65a9544bc72b) 24 | - Release 1.2.4 [`742f6c8`](https://github.com/robertsLando/Mqtt2Mqtt/commit/742f6c8a6de8e6189e701cdd4b49c2d611212012) 25 | - [fix] Show all subscriptions on debug log [`02597fd`](https://github.com/robertsLando/Mqtt2Mqtt/commit/02597fdcb3dfe39ffd0af662258d8e0391cb88e4) 26 | 27 | #### [v1.2.3](https://github.com/robertsLando/Mqtt2Mqtt/compare/v1.2.2...v1.2.3) 28 | 29 | > 26 September 2019 30 | 31 | - [feat] Clients status [`0f00847`](https://github.com/robertsLando/Mqtt2Mqtt/commit/0f0084778c966f5cc0ca9f321bdb30989046ae89) 32 | - [style] Fixed drawer behaviour [`9d3dd4c`](https://github.com/robertsLando/Mqtt2Mqtt/commit/9d3dd4cb7cef22adf562c22fd4c46e7600f013c7) 33 | - [fix] Drawer on md screens [`9ee7b8c`](https://github.com/robertsLando/Mqtt2Mqtt/commit/9ee7b8c912a1b7000462f56a0d5fa98939476426) 34 | - Release 1.2.3 [`b3a18a9`](https://github.com/robertsLando/Mqtt2Mqtt/commit/b3a18a95e06c709d0e0cc494bc12f3f10c5b85c9) 35 | - [fix] Drawer open on mobiles [`3aa2b7f`](https://github.com/robertsLando/Mqtt2Mqtt/commit/3aa2b7fccf7769d88bfab02108eab68dff6fc896) 36 | - [fix] Typo [`4eb90ec`](https://github.com/robertsLando/Mqtt2Mqtt/commit/4eb90ec7e6a0852b4ea4d6ac4499caa621db1262) 37 | - [fix] Drawer mini on init [`bbac3f5`](https://github.com/robertsLando/Mqtt2Mqtt/commit/bbac3f56ab973980164cdc616eba4a620fe294b3) 38 | - [docs] Buymeacoffie [`f6b26c6`](https://github.com/robertsLando/Mqtt2Mqtt/commit/f6b26c6faaff24becb1d3297d8570e05b77fd7d4) 39 | 40 | #### [v1.2.2](https://github.com/robertsLando/Mqtt2Mqtt/compare/v1.2.1...v1.2.2) 41 | 42 | > 8 August 2019 43 | 44 | - [fix] Fixed app title on route switch and page refresh [`72ced49`](https://github.com/robertsLando/Mqtt2Mqtt/commit/72ced49d37d0c9d48fb24af4ed289ca283e7ec37) 45 | - [feat] Split single mqtt packet into multiple packets using map function that returns Array [`9162e91`](https://github.com/robertsLando/Mqtt2Mqtt/commit/9162e918d520dc82ba50db11f09bc8dfa895f17e) 46 | - [feat] Seured mqtt client connections [`6df80ed`](https://github.com/robertsLando/Mqtt2Mqtt/commit/6df80ed547a85cefc60e788d43a3e4d08fb04255) 47 | - [fix] Fixed Vue router on production environment [`2598067`](https://github.com/robertsLando/Mqtt2Mqtt/commit/259806757d32d55f32141ef2b549530779d6690d) 48 | - [docs] Updated readme with array map function [`507c951`](https://github.com/robertsLando/Mqtt2Mqtt/commit/507c9518dcb94adc3c75e852afe81600a94273bd) 49 | - Release 1.2.2 [`5dea8f9`](https://github.com/robertsLando/Mqtt2Mqtt/commit/5dea8f91c5cb4c023171a443cd61f4f706d5523d) 50 | - [build] Fixed pkg script [`df3a6bc`](https://github.com/robertsLando/Mqtt2Mqtt/commit/df3a6bcb7bd6c8789f2e3cd95f57496733664e2f) 51 | - Create FUNDING.yml [`8d6a656`](https://github.com/robertsLando/Mqtt2Mqtt/commit/8d6a65647380de4db4ec44f1abc406b6fce2a30b) 52 | - [fix] Fixed app title [`4d37d6b`](https://github.com/robertsLando/Mqtt2Mqtt/commit/4d37d6b8972da5590d8f39e5c405976a683606d8) 53 | - Update FUNDING.yml [`75d135e`](https://github.com/robertsLando/Mqtt2Mqtt/commit/75d135e0abf068010e73b25287c3d9edad146bf1) 54 | - [fix] Back compatibility mqtt host url [`cc06c3b`](https://github.com/robertsLando/Mqtt2Mqtt/commit/cc06c3b239998002933839a74cc963dd18d923eb) 55 | 56 | #### [v1.2.1](https://github.com/robertsLando/Mqtt2Mqtt/compare/v1.2.0...v1.2.1) 57 | 58 | > 19 June 2019 59 | 60 | - [fix] Fix map set sending values to client instead of broker [`93bd7f8`](https://github.com/robertsLando/Mqtt2Mqtt/commit/93bd7f84d184dfbad60b6cf8a56de87ba3152629) 61 | - Release 1.2.1 [`54b73b8`](https://github.com/robertsLando/Mqtt2Mqtt/commit/54b73b830a6e8bf0670a44cbf5d18edc862b7d6f) 62 | - [fix] Typo broker instead aedes [`7c89876`](https://github.com/robertsLando/Mqtt2Mqtt/commit/7c89876cd7a41b814532727eb50e18767427eaf1) 63 | 64 | #### [v1.2.0](https://github.com/robertsLando/Mqtt2Mqtt/compare/v1.1.0...v1.2.0) 65 | 66 | > 13 June 2019 67 | 68 | - [style] Complete restayled broker UI and added clients status [`40054f5`](https://github.com/robertsLando/Mqtt2Mqtt/commit/40054f5466787ecdb52a2cb18dd23e40228e2b9e) 69 | - [feat] Broker settings with ssl and websockets support [`fde3972`](https://github.com/robertsLando/Mqtt2Mqtt/commit/fde3972f04f22add8b56ba2bb6962fc6db65f1ad) 70 | - [feat] Broker status management, better certs management, certs download [`43124fd`](https://github.com/robertsLando/Mqtt2Mqtt/commit/43124fdab92a2e03674969b4e7099c1117643d4d) 71 | - [fix] Better error management and styles [`398d5bb`](https://github.com/robertsLando/Mqtt2Mqtt/commit/398d5bb116488862348074863b7ba67b068fcb3c) 72 | - [docs] Updated readme and added screenshots [`13c06f4`](https://github.com/robertsLando/Mqtt2Mqtt/commit/13c06f424f0840159ec63517df684eaa360514fe) 73 | - Release 1.2.2 [`2dca64b`](https://github.com/robertsLando/Mqtt2Mqtt/commit/2dca64b91160a20cd94fadf8a3f12964c98d219a) 74 | - Release 1.2.0 [`c3556f8`](https://github.com/robertsLando/Mqtt2Mqtt/commit/c3556f86c0d38f1ad85ea046638e7e40f980b6f3) 75 | - [docs] Updated Readme [`4ea3078`](https://github.com/robertsLando/Mqtt2Mqtt/commit/4ea3078547fff523272ab4fe922a441957971679) 76 | - [fix] Fixed defaults values and broker when using ssl [`1f4fe87`](https://github.com/robertsLando/Mqtt2Mqtt/commit/1f4fe87e1db094503e294f35e69500d9b0986dbe) 77 | - [docs] Updated changelog [`f001a7b`](https://github.com/robertsLando/Mqtt2Mqtt/commit/f001a7b09ba34241d298b31e6f4277c3acef5c86) 78 | - [fix] Updated readme [`c7d102d`](https://github.com/robertsLando/Mqtt2Mqtt/commit/c7d102dbcd218168e9db6d4f7f4a7059c1006abd) 79 | - [docs] Updated readme [`50b9264`](https://github.com/robertsLando/Mqtt2Mqtt/commit/50b92644fffb23a7dba071219623f98b17e75086) 80 | - [docs] Updated changelog [`8817e01`](https://github.com/robertsLando/Mqtt2Mqtt/commit/8817e01e3f26872954f7a4f5dc67504bf4cc56d5) 81 | - [build] Updated build script [`b6a86c8`](https://github.com/robertsLando/Mqtt2Mqtt/commit/b6a86c8057d5b7da95b133815c9d57a9ab4de005) 82 | - [chore] Fixing tags that where messed up [`990d6da`](https://github.com/robertsLando/Mqtt2Mqtt/commit/990d6dafa92aa434e981fc3bcecbf922bc22284e) 83 | - Release 1.2.1 [`d2b5ed5`](https://github.com/robertsLando/Mqtt2Mqtt/commit/d2b5ed5761c263fa46757ccac7f875a9e59f6d03) 84 | - Release 1.2.0 [`02ab23d`](https://github.com/robertsLando/Mqtt2Mqtt/commit/02ab23d57a2e875a2b4e4f7da3d7fc28145fcf4b) 85 | - [fix] Removed broker inits timeout forgot in development [`0e85443`](https://github.com/robertsLando/Mqtt2Mqtt/commit/0e85443c7c894c2a16ede3c5c2469453a570190b) 86 | - [fix] Fixed authentication check [`af99a65`](https://github.com/robertsLando/Mqtt2Mqtt/commit/af99a65c3c029fd2f1a67313c70c1d170528301c) 87 | - [fix] Fixed undefined text when using custom functions [`e17a925`](https://github.com/robertsLando/Mqtt2Mqtt/commit/e17a925987966cbfc012f609b4cc322e91597f46) 88 | - [build] Fixed release script [`7367cb4`](https://github.com/robertsLando/Mqtt2Mqtt/commit/7367cb478ba7f4142206668c3bd4cbd3904d0d3c) 89 | - [fix] Release-it script [`09809bc`](https://github.com/robertsLando/Mqtt2Mqtt/commit/09809bcee9a748cc393c7325740f3f92908f4bfc) 90 | - [build] Release script [`ccdeb52`](https://github.com/robertsLando/Mqtt2Mqtt/commit/ccdeb5217cfcf8db5f1b2661dddd8a1ec83bc15a) 91 | - [fix] Removed debugger line in broker [`c475d4a`](https://github.com/robertsLando/Mqtt2Mqtt/commit/c475d4a112446648b4fd31b2a2005935de98c5a8) 92 | 93 | #### [v1.1.0](https://github.com/robertsLando/Mqtt2Mqtt/compare/1.0.0...v1.1.0) 94 | 95 | > 6 June 2019 96 | 97 | - [feat] Use a JS function in maps to map topic and payload [`244c1b8`](https://github.com/robertsLando/Mqtt2Mqtt/commit/244c1b8c0f2d20d8f3f491b18717305a29cda743) 98 | - Almost ready with client side UI [`cde59d1`](https://github.com/robertsLando/Mqtt2Mqtt/commit/cde59d186f52be9c3505da88b754077579e14881) 99 | - Working on client side [`5c00028`](https://github.com/robertsLando/Mqtt2Mqtt/commit/5c0002806df6d2e8ae4a5502a75069f122bcfbef) 100 | - Going on... [`694dfcb`](https://github.com/robertsLando/Mqtt2Mqtt/commit/694dfcb40528e07252904d17e8df4d34bfd51a9a) 101 | - Setting up main structure [`59c39d6`](https://github.com/robertsLando/Mqtt2Mqtt/commit/59c39d63f74e705ff39c802debf0f530ae2f66a8) 102 | - First working implementation [`17cead7`](https://github.com/robertsLando/Mqtt2Mqtt/commit/17cead7368eeb56a363f2ab0a3ab786efa7a2e16) 103 | - Working on server side [`d1df6d2`](https://github.com/robertsLando/Mqtt2Mqtt/commit/d1df6d2b45e9370653ac5aa59f21ef27d0c34764) 104 | - Working on store [`6e9169a`](https://github.com/robertsLando/Mqtt2Mqtt/commit/6e9169ad297036b19dad961c38359964204191d4) 105 | - Clients and values table [`695f29a`](https://github.com/robertsLando/Mqtt2Mqtt/commit/695f29a684cc3551fe492585677d4a9950bd1482) 106 | - Added values [`49b217d`](https://github.com/robertsLando/Mqtt2Mqtt/commit/49b217d99373d8a6961806254cd09f1f1dad9bff) 107 | - Working on values ialog [`081f9e8`](https://github.com/robertsLando/Mqtt2Mqtt/commit/081f9e81291ff45f02b20f01f5b63432124aac23) 108 | - Added payload maps [`010b388`](https://github.com/robertsLando/Mqtt2Mqtt/commit/010b388bb6de35ded78668a83fa608d76be663d8) 109 | - [build] Setting up Package Release and Auto-changelog scripts [`07fe3d5`](https://github.com/robertsLando/Mqtt2Mqtt/commit/07fe3d595bac3532a2af88aed5a6044ba647bdfd) 110 | - Working on pkg support [`daecdd9`](https://github.com/robertsLando/Mqtt2Mqtt/commit/daecdd92d887aa1f2b370cb6ab8d42dbdaec56fe) 111 | - Removed errors [`8b3a7d8`](https://github.com/robertsLando/Mqtt2Mqtt/commit/8b3a7d883f8f9ab21dd286c5f485753d1b348dec) 112 | - Fixed some client side problems [`fe5327e`](https://github.com/robertsLando/Mqtt2Mqtt/commit/fe5327e160d5f141a7270889da73de9a6e77641b) 113 | - Release 1.1.0 [`47b3b34`](https://github.com/robertsLando/Mqtt2Mqtt/commit/47b3b34ae6642fdfda74d0197b25bba640f2c1fb) 114 | - Updated docs [`f3404a6`](https://github.com/robertsLando/Mqtt2Mqtt/commit/f3404a6d1ee6314037bcbde69b01f1b1d235dd4b) 115 | - Updated routes and renamed payload_maps in maps [`d93524f`](https://github.com/robertsLando/Mqtt2Mqtt/commit/d93524f8b17d9de211d1a89988d4e3810d0fd4fe) 116 | - Working on readme [`ae6da74`](https://github.com/robertsLando/Mqtt2Mqtt/commit/ae6da744a739cec82ec303e1983fd0ebde96f58b) 117 | - [style] Customized scrollbar [`760e6de`](https://github.com/robertsLando/Mqtt2Mqtt/commit/760e6de1215f3d161677707909d78994a23cab39) 118 | - Better logging [`8cdeb23`](https://github.com/robertsLando/Mqtt2Mqtt/commit/8cdeb237d4f388ef248e587cccd73caec47ffe82) 119 | - Updated readme [`bf98a71`](https://github.com/robertsLando/Mqtt2Mqtt/commit/bf98a71a5ab9bebe0ec80eaa83b14b4066b6955f) 120 | - Enabled persistent storage for mqtt clients [`b67d099`](https://github.com/robertsLando/Mqtt2Mqtt/commit/b67d0999f740bf282d7d0b0d277f0c23e9e88c43) 121 | - Fixed routes [`f01300e`](https://github.com/robertsLando/Mqtt2Mqtt/commit/f01300ea6d45c9babb9a953d044223c502dc866e) 122 | - Fixed link [`1c2fc81`](https://github.com/robertsLando/Mqtt2Mqtt/commit/1c2fc8175f0afa8641737d71e680fb17b6ca54d6) 123 | - Updated sketch link [`d43e517`](https://github.com/robertsLando/Mqtt2Mqtt/commit/d43e5179d2a898d5ea05f4c0f4c9a17f0b73d9a3) 124 | - Updated link [`67d4068`](https://github.com/robertsLando/Mqtt2Mqtt/commit/67d4068bfd5616ea196521cb2bd67947dfa41afa) 125 | - Updated link [`61c223a`](https://github.com/robertsLando/Mqtt2Mqtt/commit/61c223aaa43f1c8bbc8409692df3fe5a106cee12) 126 | - Updated default store to fix init bug [`6b3dc70`](https://github.com/robertsLando/Mqtt2Mqtt/commit/6b3dc7051e69cfb6a2a9f9f7aad64e8e1931c03d) 127 | - Updated README.md [`f244fa8`](https://github.com/robertsLando/Mqtt2Mqtt/commit/f244fa88b4ddb339c9c37466ac56cfa3419f86cc) 128 | - Update .gitignore [`57e8c93`](https://github.com/robertsLando/Mqtt2Mqtt/commit/57e8c93d3099f870d957369a25a2b5eeadfb63c8) 129 | - Ignore store [`eebc9ff`](https://github.com/robertsLando/Mqtt2Mqtt/commit/eebc9ff5f10c4c2d61b1efd0f8206a246e0690d1) 130 | - Modify at https://sketchboard.me/XByU9B40UEfH [`aecee64`](https://github.com/robertsLando/Mqtt2Mqtt/commit/aecee6415272d61d1a12a283a07826c8de82bb75) 131 | - Added sketch image https://sketchboard.me/XByU9B40UEfH [`4befd90`](https://github.com/robertsLando/Mqtt2Mqtt/commit/4befd90e2220f2ec1a76fd036387fce95c8e208b) 132 | 133 | #### 1.0.0 134 | 135 | > 12 April 2019 136 | 137 | - Vue webpack init [`0c02e7a`](https://github.com/robertsLando/Mqtt2Mqtt/commit/0c02e7ae5fd2a56f16c5bdfffafeeabbb2da90e8) 138 | - Inited express [`504253b`](https://github.com/robertsLando/Mqtt2Mqtt/commit/504253b43eef5e0a9168308e7a74d61ebc1dd7d6) 139 | - Initial commit [`9402137`](https://github.com/robertsLando/Mqtt2Mqtt/commit/9402137c017b599058b615d67e73dc16468c68a7) 140 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Daniel Lando 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mqtt To Mqtt 2 | 3 | ![MQTT](images/MQTT-Logo.png) 4 | 5 | Buy Me A Coffee 6 | 7 | Fully configurable Mqtt to Mqtt gateway. 8 | 9 | - **Backend**: NodeJS, Express, Mqttjs, Aedes Mqtt Broker, Webpack 10 | - **Frontend**: Vue, [Vuetify](https://github.com/vuetifyjs/vuetify) 11 | 12 | ## Why 13 | 14 | There is almost an MQTT gateway for every protocol out there. 15 | 16 | The problem is every gateway maps reads and writes in different ways depending on the protocol used. There isn't a standard way to map a protocol in MQTT topics and also there isn't a standard payload, some sends a JSON payload with different properties (like `val` or `value` or `data` for the value and `tms` `time` `timestamp` for the timestamp/date), others use a payload with just a numeric value. 17 | 18 | This gateway inits an MQTT broker that sits between your MQTT gateways (or devices) and your broker to parse incoming/outgoing MQTT messages from/to your broker in the format you need. 19 | 20 | Here is an example schema of how it works: 21 | 22 | ![Diagram](images/sketch_diagram.png) 23 | 24 | Note here there are some bold text **GET/SET** **from/to**. More about them in docs 25 | 26 | ## :electric_plug: Installation 27 | 28 | ``` bash 29 | # Clone repo 30 | git clone https://github.com/robertsLando/ 31 | 32 | cd Mqtt2Mqtt 33 | 34 | # install dependencies 35 | npm install 36 | 37 | # build for production with minification 38 | npm run build 39 | 40 | # Start the server 41 | npm start 42 | ``` 43 | 44 | ## Usage 45 | 46 | Once the app is running open the web browser to Here you need to declare the Broker settings, Mqtt clients, the maps and (optionally) the values. 47 | 48 | ### Broker settings 49 | 50 | Used to set up the MQTT Broker. To edit settings click on the fab button in the bottom right corner 51 | 52 | - **Port**: The port where the broker will listen for incoming connections 53 | - **Websocket**: Enable this to enable MQTT over websockets 54 | - **Websocket Port**: The port where the websocket server will listen for incoming connections 55 | - **Require auth**: Enable this to enable broker authentication check 56 | - **Username/Password**: If auth is enabled insert here the username and the password to connect to this broker. 57 | - **SSL**: Enable this to enable SSL 58 | - **Self-Signed Certs**: When ssl is enabled, enable to allow self-signed certificates (used to set the flag `rejectAnauthorized`). 59 | - **Auto-generate certs**: When ssl is enabled, flag this to auto-generate certificates 60 | - **Key.pem/Cert.pem**: If auto-generate is disabled insert here the key and the certs files 61 | 62 | ### MQTT Clients 63 | 64 | Used to init a connection to an MQTT broker. To add a new client connection click on the fab button in the bottom right corner 65 | 66 | - **Name**: A unique name that identify the Client. 67 | - **Host**: The url to the broker 68 | - **Port**: Broker port 69 | - **Reconnect period**: Milliseconds between two reconnection tries 70 | - **Store**: Enable/Disable persistent storage of packets (QoS > 0). If disabled in memory storage will be used but all packets stored in memory are lost in case of shutdowns or unexpected errors. 71 | - **Clean**: Sets the clean flag when connecting to the broker 72 | - **Auth**: Enable this if broker requires auth. If so you need to enter also a valid **username** and **password**. 73 | - **Maps Get**: List of maps to use for value updates coming from other gateways to forward to your broker 74 | - **Maps Set**: List of maps to use for value writes coming from your broker to forward to the gateways. 75 | 76 | ### Maps 77 | 78 | Set of rules to use for incoming/outgoing packets. To add a new map click on the fab button in the bottom right corner 79 | 80 | - **Name**: A unique name that identify the map. 81 | - **Custom Topic**: Enable this to customize the topic 82 | - **Use function**: Enable this to use a JS function to map topic and payload. The function takes 2 args `topic` and `payload` and expects to return an object or an Array of objects like `{topic: "theNewTopic", payload: "theNewPayload"}`. If you return an Array of objects an MQTT message will be published for each Object in the Array using its payload and topic (qos, and retain flag are taken from map configuration). **ATTENTION**: Returned topic and payload must be `string`. 83 | 84 | Example code to write inside the function: 85 | 86 | ```js 87 | var parts = topic.split('/'); 88 | var sensor = parts.pop(); 89 | var data = {} 90 | 91 | data[sensor] = JSON.parse(payload).value; 92 | 93 | return {topic: topic, payload: JSON.stringify(data)} 94 | ``` 95 | 96 | > This function takes last topic level and uses it as a payload attribute and uses the payload value as value for that attribute. 97 | 98 | Example that returns an Array. 99 | 100 | ```js 101 | var topic = topic.split('/'); 102 | 103 | topic[0] = "myPrefix"; 104 | 105 | topic = topic.join('/') 106 | payload = JSON.parse(payload); 107 | 108 | var res = [] 109 | 110 | for(const k in payload){ 111 | var p = JSON.stringify({value: payload[k]}) 112 | var t = topic + '/' + k 113 | res.push({topic: t, payload: p}) 114 | } 115 | 116 | return res 117 | ``` 118 | 119 | > This example maps an incoming MQTT payload with a JSON key/value object into one MQTT message for each key of the payload with value the key value and the topic with the prefix changed and the key as suffix 120 | 121 | Example of map: 122 | 123 | Topic: `prefix/multisensor/foo` 124 | 125 | Payload: `{"temp": 10, "air": 20, "pressure": 50}` 126 | 127 | Result: 128 | 129 | - Topic: `prefix/multisensor/foo/temp` Payload: `"{"value": 10}"` 130 | - Topic: `prefix/multisensor/foo/air` Payload: `"{"value": 20}"` 131 | - Topic: `prefix/multisensor/foo/pressure` Payload: `"{"value": 50}"` 132 | 133 | - **Code**: If both custom Topic and Use functions flag are enabled this field will contain the JS code of the function used to map the topic 134 | - **Wildecard From**: The topic wildecard to use to identify packets that need to be parsed using this map. 135 | - **Wildecard To**: If custom wildecard is enabled this field is used to specify the wildecard to use to transform the original topic to the destination topic (more about this later). 136 | - **From suffix**: The suffix to add to the from wildecard. If a packet topic matches the wildecard but doesn't have this suffix it is discarded. This filed is needed because wildecards like `prefix/#/suffix` are not valid wildecards as after a # char the topic cannot have anything. 137 | - **To suffix**: The suffix to add to the destination topic after the wildecards conversion is done. 138 | - **Retain**: Sets the retain flag of the outgoing packet 139 | - **QoS**: Sets the QoS level of the outgoing packet 140 | - **Payload**: The type of payload conversion. 141 | - *Keep original*: The payload is not changed 142 | - *Value --> JSON*: The incoming payload is expected to be a raw value and I want to create a JSON object with a **Value Property** set to that value 143 | - *JSON --> Value*: The incoming payload is expected to be a JSON Object and I want to send a raw value payload using **Value Property** of original payload 144 | - *JSON --> JSON*: The incoming payload is expected to be a JSON Object and I want to map properties to another JSON object. If this option is choosed I will need to add a set of property **From** and **To** 145 | - **Add timestamp**: Can be used when destination payload is a JSON object, if enabled I can add a time property (the name of the prioperty needs to be specified in **Time property**) to outgoing packets and the value will be `Date.now()`, the timestamp when the original packet is received. 146 | 147 | ### Examples 148 | 149 | #### Wildecards 150 | 151 | | Topic From | Wildecard From | Wildecard To | Topic To | 152 | | :--------- | :------------- | :----------- | :------------- | 153 | | a/b/c/d/e | a/b/# | myprefix/# | myprefix/c/d/e | 154 | | a/b/c/d/e | +/b/+/d/e | myprefix/+/+ | myprefix/a/c | 155 | | a/b/c/d/e | +/b/# | +/my/# | a/my/c/d/e | 156 | 157 | #### Payload 158 | 159 | - **Payload**: Value --> JSON 160 | - **Value Property**: `"myvalue"` 161 | 162 | `25` ---> `{ "myvalue": 25}` 163 | 164 | --- 165 | 166 | - **Payload**: JSON --> Value 167 | - **Value Property**: `"myvalue"` 168 | 169 | `{ "myvalue": 25}` ---> `25` 170 | 171 | --- 172 | 173 | - **Payload**: JSON --> JSON 174 | - **Paylod JSON**: 175 | 176 | | From | To | 177 | | :--- | :---- | 178 | | val | value | 179 | | tms | time | 180 | 181 | `{ "val": 25, "tms": 1556548668373}` ---> `{ "value": 25, "time": 1556548668373}` 182 | 183 | ### Values 184 | 185 | If you don't want to map all values coming from the gateway but just some values you can add the values you want here using fixed topics instead of wildecards 186 | 187 | - **Client**: The Mqtt client to connect to 188 | - **Mode**: Is this value used to *GET* updates from a gateway and forwarding them to your broker or is used to *SET* values? Check the [sketch](#why) for more info about GET and SET 189 | - **Custom Topic**: Enable this to change the original topic 190 | - **Topic**: The topic where messages come from 191 | - **Custom Topic**: If **Custom topic** is enabled here you can add the destination topic where the packet received will be forwarded 192 | - **Retain**: Sets the retain flag of the outgoing packet 193 | - **QoS**: Sets the QoS level of the outgoing packet 194 | - **Payload Map**: Select the map to use to use to parse the payload, all other map values like retain, qos and wildecards are ignored 195 | 196 | ## :pencil: TODOs 197 | 198 | - [ ] Dockerize application 199 | - [x] Package application with PKG 200 | - [ ] Publish/Subscribe topics on UI for testing 201 | - [ ] Unit tests 202 | - [ ] JSON validator for settings 203 | 204 | ## :camera: Screenshots 205 | 206 | ### Broker status 207 | 208 | ![Broker](images/screen_broker.png) 209 | 210 | ### Clients table 211 | 212 | ![Clients](images/screen_clients.png) 213 | 214 | ### Values table 215 | 216 | ![Values](images/screen_values.png) 217 | 218 | ### Maps table 219 | 220 | ![Maps](images/screen_maps.png) 221 | 222 | ### Map Function 223 | 224 | ![Function](images/screen_function.png) 225 | 226 | ## :bowtie: Author 227 | 228 | [Daniel Lando](https://github.com/robertsLando) 229 | 230 | Support me on [Patreon](https://www.patreon.com/join/2409916) :heart: 231 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | reqlib = require('app-root-path').require, 3 | logger = require('morgan'), 4 | cookieParser = require('cookie-parser'), 5 | bodyParser = require('body-parser'), 6 | app = express(), 7 | jsonStore = reqlib('/lib/jsonStore.js'), 8 | store = reqlib('config/store.js'), 9 | cors = require('cors'), 10 | config = reqlib('config/app.js'), 11 | debug = reqlib('/lib/debug')('App'), 12 | broker = reqlib('lib/broker'), 13 | history = require('connect-history-api-fallback'), 14 | utils = reqlib('/lib/utils.js'); 15 | 16 | debug("Application path:" + utils.getPath(true)); 17 | 18 | // view engine setup 19 | app.set('views', utils.joinPath(utils.getPath(), 'views')); 20 | app.set('view engine', 'ejs'); 21 | 22 | app.use(history()); 23 | 24 | app.use(logger('dev')); 25 | app.use(bodyParser.json({limit: "50mb"})); 26 | app.use(bodyParser.urlencoded({limit: "50mb", extended: true, parameterLimit:50000})); 27 | app.use(cookieParser()); 28 | 29 | app.use('/', express.static(utils.joinPath(utils.getPath(), 'dist'))); 30 | 31 | app.use(cors()); 32 | 33 | // ----- APIs ------ 34 | 35 | // get settings 36 | app.get('/api/settings', function(req, res) { 37 | res.json({success:true, settings: jsonStore.get(store.settings)}); 38 | }) 39 | 40 | // update settings 41 | app.post('/api/settings', function(req, res) { 42 | jsonStore.put(store.settings, req.body) 43 | .then(data => { 44 | broker.restart(); 45 | res.json({success: true, message: "Configuration updated successfully"}) 46 | }) 47 | .catch(err => { 48 | debug(err) 49 | res.json({success: false, message: err.message}) 50 | }) 51 | }) 52 | 53 | // get settings 54 | app.get('/api/status', function(req, res) { 55 | res.json({success:true, status: broker.status()}); 56 | }) 57 | 58 | // catch 404 and forward to error handler 59 | app.use(function(req, res, next) { 60 | var err = new Error('Not Found'); 61 | err.status = 404; 62 | next(err); 63 | }); 64 | 65 | // error handler 66 | app.use(function(err, req, res, next) { 67 | // set locals, only providing error in development 68 | res.locals.message = err.message; 69 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 70 | 71 | console.log("%s %s %d - Error: %s", req.method, req.url, err.status, err.message); 72 | 73 | // render the error page 74 | res.status(err.status || 500); 75 | res.redirect('/'); 76 | }); 77 | 78 | // PROCESS MANAGEMENT 79 | 80 | process.on('SIGINT', function() { 81 | debug('Closing...'); 82 | process.exit(); 83 | }); 84 | 85 | broker.init(); 86 | 87 | //setTimeout(broker.init, 5000) 88 | 89 | module.exports = app; 90 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var reqlib = require('app-root-path').require 8 | var store = reqlib('/config/store.js') 9 | var debug = reqlib('/lib/debug')('App') 10 | var conf = reqlib('/config/app.js') 11 | var jsonStore = reqlib('/lib/jsonStore.js') 12 | var http = require('http') 13 | 14 | // jsonstore is a singleton instance that handles the json configuration files 15 | // used in the application. Init it before anything else than start app. 16 | // if jsonstore fails exit the application 17 | jsonStore.init(store) 18 | .then((data) => { 19 | 20 | var app = require('../app') 21 | 22 | /** 23 | * Normalize a port into a number, string, or false. 24 | */ 25 | 26 | function normalizePort(val) { 27 | var port = parseInt(val, 10); 28 | 29 | if (isNaN(port)) { 30 | // named pipe 31 | return val; 32 | } 33 | 34 | if (port >= 0) { 35 | // port number 36 | return port; 37 | } 38 | 39 | return false; 40 | } 41 | 42 | /** 43 | * Event listener for HTTP server "error" event. 44 | */ 45 | 46 | function onError(error) { 47 | if (error.syscall !== 'listen') { 48 | throw error; 49 | } 50 | 51 | var bind = typeof port === 'string' ? 52 | 'Pipe ' + port : 53 | 'Port ' + port; 54 | 55 | // handle specific listen errors with friendly messages 56 | switch (error.code) { 57 | case 'EACCES': 58 | console.error(bind + ' requires elevated privileges'); 59 | process.exit(1); 60 | break; 61 | case 'EADDRINUSE': 62 | console.error(bind + ' is already in use'); 63 | process.exit(1); 64 | break; 65 | default: 66 | throw error; 67 | } 68 | } 69 | 70 | /** 71 | * Event listener for HTTP server "listening" event. 72 | */ 73 | 74 | function onListening() { 75 | var addr = server.address(); 76 | var bind = typeof addr === 'string' ? 77 | 'pipe ' + addr : 78 | 'port ' + addr.port; 79 | debug('Listening on ' + bind); 80 | } 81 | 82 | /** 83 | * Get port from environment and store in Express. 84 | */ 85 | var port = normalizePort(process.env.PORT || conf.port); 86 | app.set('port', port); 87 | 88 | /** 89 | * Create HTTP server. 90 | */ 91 | 92 | var server = http.createServer(app); 93 | 94 | /** 95 | * Listen on provided port, on all network interfaces. 96 | */ 97 | 98 | server.listen(port); 99 | server.on('error', onError); 100 | server.on('listening', onListening); 101 | 102 | }) 103 | .catch(err => debug(err)) 104 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/build/logo.png -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | 12 | 13 | module.exports = { 14 | context: path.resolve(__dirname, '../'), 15 | entry: { 16 | app: './src/main.js' 17 | }, 18 | output: { 19 | path: config.build.assetsRoot, 20 | filename: '[name].js', 21 | publicPath: process.env.NODE_ENV === 'production' 22 | ? config.build.assetsPublicPath 23 | : config.dev.assetsPublicPath 24 | }, 25 | resolve: { 26 | extensions: ['.js', '.vue', '.json'], 27 | alias: { 28 | 'vue$': 'vue/dist/vue.esm.js', 29 | '@': resolve('src'), 30 | } 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.vue$/, 36 | loader: 'vue-loader', 37 | options: vueLoaderConfig 38 | }, 39 | { 40 | test: /\.js$/, 41 | loader: 'babel-loader', 42 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 43 | }, 44 | { 45 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 46 | loader: 'url-loader', 47 | options: { 48 | limit: 10000, 49 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 50 | } 51 | }, 52 | { 53 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 54 | loader: 'url-loader', 55 | options: { 56 | limit: 10000, 57 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 58 | } 59 | }, 60 | { 61 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 62 | loader: 'url-loader', 63 | options: { 64 | limit: 10000, 65 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 66 | } 67 | } 68 | ] 69 | }, 70 | node: { 71 | // prevent webpack from injecting useless setImmediate polyfill because Vue 72 | // source contains it (although only uses it if it's native). 73 | setImmediate: false, 74 | // prevent webpack from injecting mocks to Node native modules 75 | // that does not make sense for the client 76 | dgram: 'empty', 77 | fs: 'empty', 78 | net: 'empty', 79 | tls: 'empty', 80 | child_process: 'empty' 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true 59 | }), 60 | // copy custom static assets 61 | new CopyWebpackPlugin([ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.dev.assetsSubDirectory, 65 | ignore: ['.*'] 66 | } 67 | ]) 68 | ] 69 | }) 70 | 71 | module.exports = new Promise((resolve, reject) => { 72 | portfinder.basePort = process.env.PORT || config.dev.port 73 | portfinder.getPort((err, port) => { 74 | if (err) { 75 | reject(err) 76 | } else { 77 | // publish the new Port, necessary for e2e tests 78 | process.env.PORT = port 79 | // add port to devServer config 80 | devWebpackConfig.devServer.port = port 81 | 82 | // Add FriendlyErrorsPlugin 83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 84 | compilationSuccessInfo: { 85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 86 | }, 87 | onErrors: config.dev.notifyOnErrors 88 | ? utils.createNotifierCallback() 89 | : undefined 90 | })) 91 | 92 | resolve(devWebpackConfig) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = require('../config/prod.env') 15 | 16 | const webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true, 21 | usePostCSS: true 22 | }) 23 | }, 24 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 25 | output: { 26 | path: config.build.assetsRoot, 27 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 29 | }, 30 | plugins: [ 31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 32 | new webpack.DefinePlugin({ 33 | 'process.env': env 34 | }), 35 | new UglifyJsPlugin({ 36 | uglifyOptions: { 37 | compress: { 38 | warnings: false 39 | } 40 | }, 41 | sourceMap: config.build.productionSourceMap, 42 | parallel: true 43 | }), 44 | // extract css into its own file 45 | new ExtractTextPlugin({ 46 | filename: utils.assetsPath('css/[name].[contenthash].css'), 47 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 48 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 49 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 50 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 51 | allChunks: true, 52 | }), 53 | // Compress extracted CSS. We are using this plugin so that possible 54 | // duplicated CSS from different components can be deduped. 55 | new OptimizeCSSPlugin({ 56 | cssProcessorOptions: config.build.productionSourceMap 57 | ? { safe: true, map: { inline: false } } 58 | : { safe: true } 59 | }), 60 | // generate dist index.html with correct asset hash for caching. 61 | // you can customize output by editing /index.html 62 | // see https://github.com/ampedandwired/html-webpack-plugin 63 | new HtmlWebpackPlugin({ 64 | filename: config.build.index, 65 | template: 'index.html', 66 | inject: true, 67 | minify: { 68 | removeComments: true, 69 | collapseWhitespace: true, 70 | removeAttributeQuotes: true 71 | // more options: 72 | // https://github.com/kangax/html-minifier#options-quick-reference 73 | }, 74 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 75 | chunksSortMode: 'dependency' 76 | }), 77 | // keep module.id stable when vendor modules does not change 78 | new webpack.HashedModuleIdsPlugin(), 79 | // enable scope hoisting 80 | new webpack.optimize.ModuleConcatenationPlugin(), 81 | // split vendor js into its own file 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'vendor', 84 | minChunks (module) { 85 | // any required modules inside node_modules are extracted to vendor 86 | return ( 87 | module.resource && 88 | /\.js$/.test(module.resource) && 89 | module.resource.indexOf( 90 | path.join(__dirname, '../node_modules') 91 | ) === 0 92 | ) 93 | } 94 | }), 95 | // extract webpack runtime and module manifest to its own file in order to 96 | // prevent vendor hash from being updated whenever app bundle is updated 97 | new webpack.optimize.CommonsChunkPlugin({ 98 | name: 'manifest', 99 | minChunks: Infinity 100 | }), 101 | // This instance extracts shared chunks from code splitted chunks and bundles them 102 | // in a separate chunk, similar to the vendor chunk 103 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 104 | new webpack.optimize.CommonsChunkPlugin({ 105 | name: 'app', 106 | async: 'vendor-async', 107 | children: true, 108 | minChunks: 3 109 | }), 110 | 111 | // copy custom static assets 112 | new CopyWebpackPlugin([ 113 | { 114 | from: path.resolve(__dirname, '../static'), 115 | to: config.build.assetsSubDirectory, 116 | ignore: ['.*'] 117 | } 118 | ]) 119 | ] 120 | }) 121 | 122 | if (config.build.productionGzip) { 123 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 124 | 125 | webpackConfig.plugins.push( 126 | new CompressionWebpackPlugin({ 127 | asset: '[path].gz[query]', 128 | algorithm: 'gzip', 129 | test: new RegExp( 130 | '\\.(' + 131 | config.build.productionGzipExtensions.join('|') + 132 | ')$' 133 | ), 134 | threshold: 10240, 135 | minRatio: 0.8 136 | }) 137 | ) 138 | } 139 | 140 | if (config.build.bundleAnalyzerReport) { 141 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 142 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 143 | } 144 | 145 | module.exports = webpackConfig 146 | -------------------------------------------------------------------------------- /config/app.js: -------------------------------------------------------------------------------- 1 | // config/app.js 2 | module.exports = { 3 | 'storeDir': 'store', 4 | 'port': 8100 5 | }; 6 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | const appConfig = require('./app.js') 5 | 6 | 7 | module.exports = merge(prodEnv, { 8 | NODE_ENV: '"development"', 9 | PORT: appConfig.port 10 | }) 11 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8101, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | 24 | /** 25 | * Source Maps 26 | */ 27 | 28 | // https://webpack.js.org/configuration/devtool/#development 29 | devtool: 'cheap-module-eval-source-map', 30 | 31 | // If you have problems debugging vue-files in devtools, 32 | // set this to false - it *may* help 33 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 34 | cacheBusting: true, 35 | 36 | cssSourceMap: true 37 | }, 38 | 39 | build: { 40 | // Template for index.html 41 | index: path.resolve(__dirname, '../dist/index.html'), 42 | 43 | // Paths 44 | assetsRoot: path.resolve(__dirname, '../dist'), 45 | assetsSubDirectory: 'static', 46 | assetsPublicPath: '/', 47 | 48 | /** 49 | * Source Maps 50 | */ 51 | 52 | productionSourceMap: true, 53 | // https://webpack.js.org/configuration/devtool/#production 54 | devtool: '#source-map', 55 | 56 | // Gzip off by default as many popular static hosts such as 57 | // Surge or Netlify already gzip all static assets for you. 58 | // Before setting to `true`, make sure to: 59 | // npm install --save-dev compression-webpack-plugin 60 | productionGzip: false, 61 | productionGzipExtensions: ['js', 'css'], 62 | 63 | // Run the build command with an extra argument to 64 | // View the bundle analyzer report after build finishes: 65 | // `npm run build --report` 66 | // Set to `true` or `false` to always turn it on or off 67 | bundleAnalyzerReport: process.env.npm_config_report 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /config/store.js: -------------------------------------------------------------------------------- 1 | // config/store.js 2 | module.exports = { 3 | 'settings' : {file: 'settings.json', default: {clients: [], values: [], maps: [], broker: {port: 1884}}} , 4 | 'certs' : {file: 'certs.json', default: {}} , 5 | }; 6 | -------------------------------------------------------------------------------- /images/MQTT-Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/images/MQTT-Logo.png -------------------------------------------------------------------------------- /images/screen_broker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/images/screen_broker.png -------------------------------------------------------------------------------- /images/screen_clients.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/images/screen_clients.png -------------------------------------------------------------------------------- /images/screen_function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/images/screen_function.png -------------------------------------------------------------------------------- /images/screen_maps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/images/screen_maps.png -------------------------------------------------------------------------------- /images/screen_values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/images/screen_values.png -------------------------------------------------------------------------------- /images/sketch_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/images/sketch_diagram.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MQTT To MQTT 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /lib/MqttClient.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // eslint-disable-next-line one-var 4 | var reqlib = require('app-root-path').require, 5 | mqtt = require('mqtt'), 6 | utils = reqlib('/lib/utils.js'), 7 | NeDBStore = require('mqtt-nedb-store'), 8 | EventEmitter = require('events'), 9 | storeDir = reqlib('config/app.js').storeDir, 10 | debug = reqlib('/lib/debug'), 11 | inherits = require('util').inherits, 12 | url = require('url') 13 | 14 | const NAME_PREFIX = 'M2M-' 15 | 16 | /** 17 | * The constructor 18 | */ 19 | function MqttClient (config) { 20 | if (!(this instanceof MqttClient)) { 21 | return new MqttClient(config) 22 | } 23 | EventEmitter.call(this) 24 | init.call(this, config) 25 | } 26 | 27 | inherits(MqttClient, EventEmitter) 28 | 29 | function init (config) { 30 | this.config = config 31 | this.toSubscribe = [] 32 | 33 | this.clientID = this.cleanName(NAME_PREFIX + config.name); 34 | 35 | this.debug = debug(this.clientID); 36 | 37 | var parsed = url.parse(config.host || '') 38 | var protocol = 'mqtt' 39 | 40 | if (parsed.protocol) protocol = parsed.protocol.replace(/:$/, '') 41 | 42 | var options = { 43 | clientId: this.clientID, 44 | reconnectPeriod: config.reconnectPeriod, 45 | clean: config.clean, 46 | rejectUnauthorized: !config.allowSelfsigned, 47 | protocol: protocol, 48 | host: parsed.hostname || config.host, 49 | port: config.port, 50 | // will: { 51 | // topic: this.getClientTopic(), 52 | // payload: JSON.stringify({value: false}), 53 | // qos: 1, 54 | // retain: true 55 | // } 56 | } 57 | 58 | if (['mqtts', 'wss', 'wxs', 'alis', 'tls'].indexOf(protocol) >= 0) { 59 | if (!config.allowSelfsigned) options.ca = config._ca 60 | options.key = config._key 61 | options.cert = config._cert 62 | } 63 | 64 | if (config.store) { 65 | const COMPACT = { autocompactionInterval: 30000 } 66 | var manager = NeDBStore(utils.joinPath(utils.getPath(true), storeDir, this.clientID), {incoming: COMPACT, outgoing: COMPACT}) 67 | options.incomingStore = manager.incoming 68 | options.outgoingStore = manager.outgoing 69 | } 70 | 71 | if (config.auth) { 72 | options.username = config.username 73 | options.password = config.password 74 | } 75 | 76 | try { 77 | var client = mqtt.connect(options) 78 | 79 | this.client = client 80 | 81 | client.on('connect', onConnect.bind(this)) 82 | client.on('message', onMessageReceived.bind(this)) 83 | client.on('reconnect', onReconnect.bind(this)) 84 | client.on('close', onClose.bind(this)) 85 | client.on('error', onError.bind(this)) 86 | client.on('offline', onOffline.bind(this)) 87 | } catch (e) { 88 | this.debug('Error while connecting MQTT', e.message) 89 | this.error = e.message 90 | } 91 | } 92 | 93 | /** 94 | * Function called when MQTT client connects 95 | */ 96 | function onConnect () { 97 | this.debug('CONNECTED') 98 | this.emit('connect') 99 | 100 | if (this.toSubscribe) { 101 | for (var i = 0; i < this.toSubscribe.length; i++) { 102 | this.subscribe(this.toSubscribe[i]) 103 | } 104 | } 105 | 106 | // Update client status 107 | // this.updateClientStatus(true) 108 | 109 | this.toSubscribe = [] 110 | } 111 | 112 | /** 113 | * Function called when MQTT client reconnects 114 | */ 115 | function onReconnect () { 116 | this.debug('RECONNECTING') 117 | } 118 | 119 | /** 120 | * Function called when MQTT client reconnects 121 | */ 122 | function onError (error) { 123 | this.debug(error.message) 124 | this.error = error.message 125 | } 126 | 127 | /** 128 | * Function called when MQTT client go offline 129 | */ 130 | function onOffline () { 131 | this.debug('OFFLINE') 132 | } 133 | 134 | /** 135 | * Function called when MQTT client is closed 136 | */ 137 | function onClose () { 138 | this.debug('CLOSED') 139 | } 140 | 141 | /** 142 | * Function called when an MQTT message is received 143 | */ 144 | function onMessageReceived (topic, payload) { 145 | this.debug('Message received on', topic); 146 | 147 | payload = payload ? payload.toString() : payload; 148 | 149 | this.emit('writeRequest', topic, payload) 150 | 151 | }// end onMessageReceived 152 | 153 | MqttClient.prototype.cleanName = function (name) { 154 | if (!isNaN(name)) return name 155 | 156 | name = name.replace(/\s/g, '_') 157 | return name.replace(/[+*#\\.''``!?^=(),""%[\]:;{}]+/g, '') 158 | } 159 | 160 | /** 161 | * Method used to close clients connection, use this before destroy 162 | */ 163 | MqttClient.prototype.close = function (cb) { 164 | if (this.closed) { 165 | cb() 166 | } 167 | 168 | this.closed = true 169 | 170 | var self = this 171 | 172 | function done () { 173 | self.removeAllListeners() 174 | 175 | if (self.client) { 176 | self.client.removeAllListeners() 177 | } 178 | 179 | cb() 180 | } 181 | 182 | if (this.client) { 183 | this.client.end(!this.client.connected, {}, done) 184 | } else done() 185 | } 186 | 187 | /** 188 | * Method used to get status 189 | */ 190 | MqttClient.prototype.getStatus = function () { 191 | var status = {} 192 | 193 | status.status = this.client && this.client.connected 194 | status.error = this.error || 'Offline' 195 | status.config = this.config 196 | 197 | return status 198 | } 199 | 200 | /** 201 | * Method used to update client connection status 202 | */ 203 | // MqttClient.prototype.updateClientStatus = function (connected, ...devices) { 204 | // var topic = "test/test" 205 | // this.client.publish(topic, JSON.stringify({value: connected, time: Date.now()}), {retain: true, qos: 1}) 206 | // } 207 | 208 | /** 209 | * Method used to update client 210 | */ 211 | MqttClient.prototype.update = function (config) { 212 | this.close() 213 | 214 | this.debug(`RESTARTING`) 215 | 216 | init.call(this, config) 217 | } 218 | 219 | /** 220 | * Method used to subscribe for write requests 221 | */ 222 | MqttClient.prototype.subscribe = function (topic) { 223 | if (this.client && this.client.connected) { 224 | this.client.subscribe(topic, (err) => err && this.debug("Error while subscribing: %s", err.message)) 225 | } else { this.toSubscribe.push(topic) } 226 | } 227 | 228 | /** 229 | * Method used to publish an update 230 | */ 231 | MqttClient.prototype.publish = function (topic, data, options) { 232 | if (this.client) { 233 | this.client.publish(topic, data, options, function (err) { 234 | if (err) { this.debug('Error while publishing a value', err.message) } 235 | }) 236 | } // end if client 237 | } 238 | 239 | /** 240 | * Used to get client connection status 241 | */ 242 | Object.defineProperty(MqttClient.prototype, 'connected', { 243 | get: function () { 244 | return this.client && this.client.connected 245 | }, 246 | enumerable: true 247 | }) 248 | 249 | /** 250 | * Used to get client ID 251 | */ 252 | Object.defineProperty(MqttClient.prototype, 'client_id', { 253 | get: function () { 254 | return this.clientID 255 | }, 256 | enumerable: true 257 | }) 258 | 259 | 260 | module.exports = MqttClient 261 | -------------------------------------------------------------------------------- /lib/broker.js: -------------------------------------------------------------------------------- 1 | var reqlib = require('app-root-path').require 2 | var config = reqlib('config/app.js') 3 | var aedes = require('aedes')() 4 | var jsonStore = reqlib('/lib/jsonStore.js') 5 | var store = reqlib('config/store.js') 6 | var debug = reqlib('/lib/debug')('Broker') 7 | var MqttClient = reqlib('/lib/MqttClient') 8 | var mqttWildcard = require('mqtt-wildcard') 9 | const Promise = require('bluebird') 10 | 11 | const VALID_DAYS = 9999; 12 | 13 | const createCertificate = Promise.promisify(require("pem").createCertificate, { 14 | multiArgs: true 15 | }) 16 | 17 | var settings = {}; 18 | var config = {}; 19 | 20 | var server, wsServer; 21 | 22 | var brokerError = "Not connected" 23 | var wsError = "Not connected"; 24 | 25 | var maps = {}; 26 | var maps_keys = []; 27 | var clients = []; 28 | var topicsGet = {}; 29 | 30 | aedes.authenticate = function (client, username, password, callback) { 31 | if (password) password = password.toString(); 32 | 33 | var success = !config.authenticate || (username === config.username && password === config.password); 34 | callback(null, success); 35 | } 36 | 37 | // client connected 38 | aedes.on('client', (client) => { 39 | debug("Client CONNECTED:", client.id) 40 | 41 | if(config.clientsStatus) { 42 | aedes.publish({ 43 | topic: config.statusTopic.replace(/\$ID/g, client.id), 44 | qos: config.statusQos || 0, 45 | retain: config.statusRetain, 46 | payload: 'true' 47 | }) 48 | } 49 | }); 50 | 51 | // client disconnected 52 | aedes.on('clientDisconnect', (client) => { 53 | debug("Client DISCONNECTED:", client.id) 54 | 55 | if(config.clientsStatus) { 56 | aedes.publish({ 57 | topic: config.statusTopic.replace(/\$ID/g, client.id), 58 | qos: config.statusQos || 0, 59 | retain: config.statusRetain, 60 | payload: 'false' 61 | }) 62 | } 63 | }); 64 | 65 | // client subscribed to topic 66 | aedes.on('subscribe', (subscriptions, client) => { 67 | debug("Client %s SUBSCRIED to %s:", client.id, subscriptions[0].topic) 68 | }); 69 | 70 | // client unsubscribed to topic 71 | aedes.on('unsubscribe', (subscriptions, client) => { 72 | debug("Client %s UNSUBSCRIBED to %s:", client.id, ...subscriptions) 73 | }); 74 | 75 | // a client has publish a packet 76 | aedes.on('publish', (packet, client) => { 77 | 78 | var topic = packet.topic; 79 | 80 | if (topic.startsWith('$SYS')) return; // System message 81 | 82 | // When client is null the client is the broker itself 83 | if (!client) client = { 84 | id: "$BROKER" 85 | } 86 | 87 | var payload = packet.payload ? packet.payload.toString() : payload; 88 | 89 | debug("Client %s PUBLISH on %s: %s", client.id, packet.topic, packet.payload); 90 | 91 | if (topicsGet[topic]) { 92 | mapPacketValue(topic, payload, topicsGet[topic]) 93 | } else { 94 | var toSend = []; 95 | 96 | // Check if there is a valid map for the incoming message topic 97 | for (let i = 0, len = maps_keys.length; i < len; i++) { 98 | if (mqttWildcard(topic, maps_keys[i])) { 99 | toSend.push(maps[maps_keys[i]]); 100 | } 101 | } 102 | 103 | // Foreach valid map forward the message to the destination client with custom map rules 104 | toSend.forEach(map => { 105 | mapPacket(topic, payload, map) 106 | }); 107 | 108 | } 109 | 110 | }); 111 | 112 | 113 | /** 114 | * Inits the Broker 115 | * 116 | */ 117 | async function init() { 118 | 119 | try { 120 | 121 | // get a deep copy of settings 122 | settings = JSON.parse(JSON.stringify(jsonStore.get(store.settings))); 123 | 124 | config = settings.broker || {}; 125 | 126 | if (!config.port) config.port = 1884; 127 | 128 | if (config.ssl) { 129 | var now = new Date(); 130 | 131 | if (config.autoGenerate) { 132 | var certs = jsonStore.get(store.certs); 133 | var expired = certs.expireDate ? (new Date(certs.expireDate)).getTime() < now.getTime() : true; 134 | 135 | if (expired || !certs.key || !certs.cert) { 136 | var keys = await createCertificate({ 137 | days: VALID_DAYS, 138 | selfSigned: true 139 | }); 140 | 141 | config._key = certs.key = keys[0].serviceKey 142 | config._cert = certs.cert = keys[0].certificate 143 | now.setDate(now.getDate() + VALID_DAYS); 144 | 145 | certs.expireDate = now; 146 | 147 | await jsonStore.put(store.certs, certs); 148 | } else { 149 | config._key = certs.key; 150 | config._cert = certs.cert; 151 | } 152 | } 153 | 154 | var options = { 155 | key: config._key, 156 | cert: config._cert, 157 | rejectUnauthorized: !config.selfSigned 158 | } 159 | 160 | server = require('tls').createServer(options, aedes.handle) 161 | 162 | } else { 163 | 164 | server = require('net').createServer(aedes.handle) 165 | } 166 | 167 | server.on('error', (err) => { 168 | debug(err.message); 169 | brokerError = err.code == "EADDRINUSE" ? "Port " + config.port + " is already used" : err.message 170 | }) 171 | 172 | server.listen(config.port, function () { 173 | debug('Listening on port', config.port) 174 | }); 175 | 176 | if (config.websocket) { 177 | var ws = require('websocket-stream'); 178 | 179 | if (config.ssl) { 180 | wsServer = require('https').createServer({ 181 | key: config._key, 182 | cert: config._cert, 183 | //requestCert: true, // client send their certificate 184 | rejectUnauthorized: !config.selfSigned 185 | }); 186 | } else { 187 | wsServer = require('http').createServer(); 188 | } 189 | 190 | ws.createServer({ 191 | server: wsServer 192 | }, aedes.handle).on('error', (err) => { 193 | debug(err.message) 194 | wsError = err.code == "EADDRINUSE" ? "Port " + config.webPort + " is already used" : err.message 195 | }) 196 | 197 | wsServer.listen(config.webPort, "0.0.0.0", function () { 198 | debug('MQTT websocket server listening on port', config.webPort) 199 | }); 200 | 201 | } 202 | 203 | maps = {}; 204 | maps_keys = []; 205 | clients = []; 206 | topicsGet = {}; 207 | 208 | // init mqtt clients 209 | settings.clients.forEach(c => { 210 | initClient(c) 211 | }); 212 | 213 | config.error = null; 214 | 215 | } catch (error) { 216 | config.error = error; 217 | } 218 | } 219 | 220 | 221 | /** 222 | * Inits an Mqtt Client with given client configuration 223 | * 224 | * @param {Object} c Client configuration Object 225 | */ 226 | function initClient(c) { 227 | var client = new MqttClient(c); 228 | 229 | clients.push(client); 230 | 231 | var mapsGet = c.mapsGet.map(id => settings.maps.find(c => c._id == id)); 232 | var mapsSet = c.mapsSet.map(id => settings.maps.find(c => c._id == id)); 233 | 234 | // remove null values from array 235 | mapsSet = mapsSet.filter(e => !!e) 236 | mapsGet = mapsGet.filter(e => !!e) 237 | 238 | mapsGet.forEach(m => { 239 | m.client = client; 240 | maps[m.wFrom] = m 241 | maps_keys.push(m.wFrom); 242 | }); 243 | 244 | // subscribe for write requests 245 | mapsSet.forEach(m => { 246 | client.subscribe(m.wFrom); 247 | }); 248 | 249 | // subscribe to client values 250 | var values = settings.values ? settings.values.filter(v => v.client_id === c._id && v.mode === "SET") : []; 251 | var topicsSet = []; 252 | 253 | values.forEach(v => { 254 | v.map = settings.maps.find(m => m._id == v.map_id); 255 | if (v.map) { // if there is a valid map subscribe for updates 256 | client.subscribe(v.from); 257 | topicsSet[v.from] = v; 258 | } 259 | }); 260 | 261 | // Popolate topicsGet object for broker incoming messages 262 | values = settings.values ? settings.values.filter(v => v.client_id === c._id && v.mode === "GET") : []; 263 | 264 | values.forEach(v => { 265 | v.map = settings.maps.find(m => m._id == v.map_id); 266 | v.client = client; 267 | if (v.map) { // if there is a valid map add topic to the topicsGet object 268 | topicsGet[v.from] = v; 269 | } 270 | }); 271 | 272 | // used to suppress duplicated messages when using both values and client maps and a map 273 | // is a wildecard of a topic value (so 2 messages are received one for ecah subscription) 274 | var ignoreNext = false; 275 | 276 | // handle write requests 277 | client.on("writeRequest", function (topic, payload) { 278 | 279 | if (ignoreNext) { 280 | ignoreNext = false; 281 | return; 282 | } 283 | 284 | // there is a value for this topic 285 | if (topicsSet[topic]) { 286 | 287 | if (mapsSet.find(m => mqttWildcard(topic, m.wFrom))) 288 | ignoreNext = true; 289 | 290 | mapPacketValue(topic, payload, topicsSet[topic]) 291 | 292 | } else { // check for maps 293 | // get the first map wildecard corresponding to the topic 294 | var map = mapsSet.find(m => mqttWildcard(topic, m.wFrom)); 295 | 296 | if (map) { 297 | mapPacket(topic, payload, map) 298 | } 299 | } 300 | }); 301 | } 302 | 303 | 304 | /** 305 | * Maps an MQTT packet using a Value Configuration 306 | * 307 | * @param {String} topic Topic of the received message 308 | * @param {String} payload Payload received 309 | * @param {Object} val Value Configuration Object 310 | */ 311 | function mapPacketValue(topic, payload, val) { 312 | // set the custom topic 313 | if (val.customTopic) topic = val.to; 314 | else if (val.map.useFunction) { 315 | try { 316 | var res = mapFunction(topic, payload, val.map) 317 | 318 | for (let i = 0; i < res.length; i++) 319 | publishPacket(val, res[i].topic, res[i].payload); 320 | 321 | // stop here 322 | return; 323 | } catch (err) { 324 | debug("Function error while mapping", topic, payload, err.message) 325 | return; 326 | } 327 | } 328 | 329 | // map the payload 330 | try { 331 | payload = mapPayload(val.map, payload); 332 | } catch (e) { 333 | debug("Error while mapping payload", e.message) 334 | return; 335 | } 336 | 337 | publishPacket(val, topic, payload) 338 | } 339 | 340 | function mapFunction(topic, payload, map) { 341 | var func = new Function('topic', 'payload', map.code); 342 | var res = func(topic, payload); 343 | 344 | if (!Array.isArray(res)) res = [res] 345 | 346 | for (let i = 0; i < res.length; i++) { 347 | if (!res[i].topic || typeof res[i].topic != 'string') 348 | throw Error("returned topic must exist and must be a string. Got: " + res[i].topic) 349 | 350 | if (!res[i].payload || typeof res[i].payload != 'string') 351 | throw Error("returned payload must exist must be a string. Got: " + res[i].payload) 352 | } 353 | 354 | return res; 355 | } 356 | 357 | 358 | /** 359 | * Maps an MQTT Packet using a Map Configuration 360 | * 361 | * @param {String} topic The MQTT Topic String 362 | * @param {String} payload The MQTT payload String 363 | * @param {String} map The Map configuration Object 364 | * @returns 365 | */ 366 | function mapPacket(topic, payload, map) { 367 | 368 | // map topic if customTopic is true 369 | if (map.customTopic) { 370 | 371 | if (map.useFunction) { 372 | try { 373 | var res = mapFunction(topic, payload, map) 374 | 375 | for (let i = 0; i < res.length; i++) 376 | publishPacket(map, res[i].topic, res[i].payload); 377 | 378 | // stop here 379 | return; 380 | } catch (err) { 381 | debug("Function error while mapping", topic, payload, err.message) 382 | return; 383 | } 384 | } else { 385 | 386 | if (map.suffixFrom) { 387 | if (!topic.endsWith(map.suffixFrom)) return; 388 | else topic = topic.substr(0, topic.indexOf(map.suffixFrom) - 1); 389 | } 390 | 391 | var parts = map.wTo.split('/') 392 | 393 | // returns an array with the wildecards parts 394 | var wParts = mqttWildcard(topic, map.wFrom); 395 | 396 | // replace array wildecards in parts wildecards 397 | for (let i = 0, k = 0; i < parts.length && k < wParts.length; i++) { 398 | if (parts[i] == "#" || parts[i] == "+") { 399 | parts[i] = wParts[k++]; 400 | } 401 | } 402 | 403 | if (map.suffixTo) parts.push(map.suffixTo); 404 | 405 | // recreate destination topic 406 | topic = parts.join('/'); 407 | } 408 | } 409 | 410 | // map payload 411 | if (!map.useFunction) { 412 | try { 413 | payload = mapPayload(map, payload); 414 | } catch (e) { 415 | debug("Error while mapping payload", e.message) 416 | return; 417 | } 418 | } 419 | 420 | publishPacket(map, topic, payload) 421 | } 422 | 423 | 424 | /** 425 | * Publish a packet using map/value client or Aedes Broker 426 | * 427 | * @param {Object} mv Map or Value Configuration Object 428 | * @param {String} topic The MQTT Topic String 429 | * @param {String} payload The MQTT payload String 430 | */ 431 | function publishPacket(mv, topic, payload) { 432 | if (mv.client) { 433 | var options = { 434 | qos: mv.qos, 435 | retain: mv.retain 436 | } 437 | mv.client.publish(topic, payload, options); 438 | } else { 439 | aedes.publish({ 440 | topic: topic, 441 | qos: mv.qos, 442 | retain: mv.retain, 443 | payload: payload 444 | }) 445 | } 446 | } 447 | 448 | function mapPayload(map, payload) { 449 | 450 | switch (map.payload) { 451 | case 0: 452 | // keep original 453 | break; 454 | case 1: 455 | // value --> JSON 456 | payload[map.payloadValue] = parseInt(payload); 457 | if (map.addTime) payload[map.timeValue] = Date.now(); 458 | return JSON.stringify(payload); 459 | case 2: 460 | // JSON --> value 461 | payload = JSON.parse(payload); 462 | return payload[map.payloadValue].toString() 463 | case 3: 464 | // JSON --> JSON 465 | let tmp = {}; 466 | payload = JSON.parse(payload); 467 | map.payloadMap.forEach(m => { 468 | tmp[m.to] = payload[m.from]; 469 | }); 470 | if (map.addTime) tmp[map.timeValue] = Date.now(); 471 | return JSON.stringify(tmp); 472 | default: 473 | debug("Unknown map function of map %s: %s", map.name, map.payload) 474 | break; 475 | } 476 | 477 | return payload; 478 | } 479 | 480 | 481 | /** 482 | * Close all MQTT Clients and Broker 483 | * 484 | */ 485 | function close(cb) { 486 | 487 | function done() { 488 | if(--count === 0) cb() 489 | } 490 | 491 | var toClose = [...clients, wsServer, server] 492 | var count = toClose.length 493 | 494 | if(count === 0) { 495 | cb() 496 | } else { 497 | toClose.forEach(c => { 498 | if(c) c.close(done); 499 | else done() 500 | }); 501 | } 502 | 503 | } 504 | 505 | 506 | /** 507 | * Restart all Clients and Broker 508 | * 509 | */ 510 | function restart() { 511 | close(init); 512 | } 513 | 514 | 515 | /** 516 | * Get Broker and Clients Status 517 | * 518 | * @returns An Object with all status parameters 519 | */ 520 | function status() { 521 | return { 522 | broker: server ? server.address() : null, 523 | websocket: wsServer ? wsServer.address() : null, 524 | clients: clients.map(c => c.getStatus()), 525 | wsError: wsError, 526 | brokerError: brokerError, 527 | ssl: config.ssl, 528 | key: config._key, 529 | cert: config._cert 530 | } 531 | } 532 | 533 | 534 | module.exports = exports = { 535 | init: init, 536 | close: close, 537 | restart: restart, 538 | status: status 539 | } 540 | -------------------------------------------------------------------------------- /lib/debug.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug') 2 | 3 | debug.enable('mqtt2mqtt:*') 4 | debug = debug('mqtt2mqtt') 5 | 6 | debug.log = console.log.bind(console) 7 | 8 | module.exports = function (namespace) { return debug.extend(namespace) } 9 | -------------------------------------------------------------------------------- /lib/jsonStore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // eslint-disable-next-line one-var 4 | var jsonfile = require('jsonfile'), 5 | reqlib = require('app-root-path').require, 6 | storeDir = reqlib('config/app.js').storeDir, 7 | Promise = require('bluebird'), 8 | debug = reqlib('/lib/debug')('Store'), 9 | utils = reqlib('lib/utils.js') 10 | 11 | debug.color = 3 12 | 13 | function getFile (config) { 14 | return new Promise((resolve, reject) => { 15 | jsonfile.readFile(utils.joinPath(utils.getPath(true), storeDir, config.file), function (err, data) { 16 | if (err && err.code !== 'ENOENT') { 17 | reject(err) 18 | } else { 19 | if (err && err.code === 'ENOENT') { debug(config.file, 'not found') } 20 | 21 | resolve({file: config.file, data: data || config.default}) 22 | } 23 | }) 24 | }) 25 | } 26 | 27 | /** 28 | Constructor 29 | **/ 30 | function StorageHelper () { 31 | this.store = {} 32 | } 33 | 34 | StorageHelper.prototype.init = function (config) { 35 | return new Promise((resolve, reject) => { 36 | storage_helper.config = config 37 | Promise.map(Object.keys(config), function (model) { 38 | return getFile(config[model]) 39 | }).then(results => { 40 | for (var i = 0; i < results.length; i++) { 41 | storage_helper.store[results[i].file] = results[i].data 42 | } 43 | resolve(storage_helper.store) 44 | }) 45 | .catch(err => reject(err)) 46 | }) 47 | } 48 | 49 | StorageHelper.prototype.get = function (model) { 50 | if (storage_helper.store[model.file]) { return storage_helper.store[model.file] } else { throw Error('Requested file not present in store: ' + model.file) } 51 | } 52 | 53 | StorageHelper.prototype.put = function (model, data) { 54 | return new Promise((resolve, reject) => { 55 | jsonfile.writeFile(utils.joinPath(utils.getPath(true), storeDir, model.file), data, function (err) { 56 | if (err) { 57 | reject(err) 58 | } else { 59 | storage_helper.store[model.file] = data 60 | resolve(storage_helper.store[model.file]) 61 | } 62 | }) 63 | }) 64 | } 65 | 66 | // eslint-disable-next-line camelcase 67 | var storage_helper = module.exports = exports = new StorageHelper() 68 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line one-var 2 | var appRoot = require('app-root-path'), 3 | path = require('path') 4 | 5 | module.exports = { 6 | getPath (write) { 7 | if (write && process.pkg) return process.cwd() 8 | else return appRoot.toString() 9 | }, 10 | joinPath (...paths) { 11 | return path.join(...paths) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqtt2mqtt", 3 | "version": "1.2.5", 4 | "bin": "bin/www", 5 | "description": "Configurable Mqtt gateway", 6 | "author": "Daniel Lando ", 7 | "private": false, 8 | "pkg": { 9 | "scripts": [ 10 | "lib/**/*.js", 11 | "config/**/*.js", 12 | "app.js" 13 | ], 14 | "assets": [ 15 | "views/**/*", 16 | "static/**/*", 17 | "dist/**/*" 18 | ] 19 | }, 20 | "scripts": { 21 | "dev": "webpack-dev-server --inline --progress --host 0.0.0.0 --config build/webpack.dev.conf.js", 22 | "dev:server": "nodemon bin/www", 23 | "start": "node bin/www", 24 | "lint": "eslint --ext .js,.vue src", 25 | "build": "node build/build.js", 26 | "pkg": "sudo chmod +x package.sh && ./package.sh", 27 | "changelog": "auto-changelog -p && git add CHANGELOG.md", 28 | "release": "read -p 'GITHUB_TOKEN: ' GITHUB_TOKEN && export GITHUB_TOKEN=$GITHUB_TOKEN && release-it" 29 | }, 30 | "release-it": { 31 | "github": { 32 | "release": true, 33 | "assets": [ 34 | "pkg/*.zip" 35 | ] 36 | }, 37 | "git": { 38 | "tagName": "v${version}" 39 | }, 40 | "hooks": { 41 | "after:bump": "npm run changelog", 42 | "before:release": "./package.sh --release" 43 | }, 44 | "npm": { 45 | "publish": false 46 | } 47 | }, 48 | "auto-changelog": { 49 | "unreleased": true, 50 | "commitLimit": false, 51 | "replaceText": { 52 | "^-[\\s]*": "" 53 | } 54 | }, 55 | "dependencies": { 56 | "aedes": "^0.42.5", 57 | "app-root-path": "^2.2.1", 58 | "axios": "^0.18.0", 59 | "axios-progress-bar": "^1.2.0", 60 | "babel-polyfill": "^6.26.0", 61 | "bluebird": "^3.5.4", 62 | "body-parser": "^1.18.3", 63 | "connect-history-api-fallback": "^1.6.0", 64 | "cookie-parser": "~1.4.3", 65 | "cors": "^2.8.5", 66 | "debug": "^4.1.1", 67 | "ejs": "~2.5.7", 68 | "express": "~4.16.0", 69 | "http-errors": "~1.6.2", 70 | "install": "^0.12.2", 71 | "jsonfile": "^5.0.0", 72 | "morgan": "~1.9.0", 73 | "mqtt": "^4.1.0", 74 | "mqtt-nedb-store": "^0.1.1", 75 | "mqtt-wildcard": "^3.0.9", 76 | "nedb": "^1.8.0", 77 | "npm": "^6.9.0", 78 | "pem": "^1.14.2", 79 | "prismjs": "^1.16.0", 80 | "uniqid": "^5.0.3", 81 | "vue": "^2.5.2", 82 | "vue-prism-editor": "^0.2.1", 83 | "vue-router": "^3.0.1", 84 | "vuetify": "^1.5.11", 85 | "vuex": "^3.1.0" 86 | }, 87 | "devDependencies": { 88 | "autoprefixer": "^7.1.2", 89 | "babel-core": "^6.22.1", 90 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 91 | "babel-loader": "^7.1.1", 92 | "babel-plugin-syntax-jsx": "^6.18.0", 93 | "babel-plugin-transform-runtime": "^6.22.0", 94 | "babel-plugin-transform-vue-jsx": "^3.5.0", 95 | "babel-preset-env": "^1.3.2", 96 | "babel-preset-stage-2": "^6.22.0", 97 | "chalk": "^2.0.1", 98 | "copy-webpack-plugin": "^4.0.1", 99 | "css-loader": "^0.28.0", 100 | "extract-text-webpack-plugin": "^3.0.0", 101 | "file-loader": "^1.1.4", 102 | "friendly-errors-webpack-plugin": "^1.6.1", 103 | "html-webpack-plugin": "^2.30.1", 104 | "node-notifier": "^5.1.2", 105 | "optimize-css-assets-webpack-plugin": "^3.2.0", 106 | "ora": "^1.2.0", 107 | "portfinder": "^1.0.13", 108 | "postcss-import": "^11.0.0", 109 | "postcss-loader": "^2.0.8", 110 | "postcss-url": "^7.2.1", 111 | "rimraf": "^2.6.0", 112 | "semver": "^5.3.0", 113 | "shelljs": "^0.7.6", 114 | "uglifyjs-webpack-plugin": "^1.1.1", 115 | "url-loader": "^0.5.8", 116 | "vue-loader": "^13.3.0", 117 | "vue-style-loader": "^3.0.1", 118 | "vue-template-compiler": "^2.5.2", 119 | "webpack": "^3.6.0", 120 | "webpack-bundle-analyzer": "^2.9.0", 121 | "webpack-dev-server": "^2.9.1", 122 | "webpack-merge": "^4.1.0" 123 | }, 124 | "engines": { 125 | "node": ">= 6.0.0", 126 | "npm": ">= 3.0.0" 127 | }, 128 | "browserslist": [ 129 | "> 1%", 130 | "last 2 versions", 131 | "not ie <= 8" 132 | ] 133 | } 134 | -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | ask() { 5 | # http://djm.me/ask 6 | while true; do 7 | 8 | if [ "${2:-}" = "Y" ]; then 9 | prompt="Y/n" 10 | default=Y 11 | elif [ "${2:-}" = "N" ]; then 12 | prompt="y/N" 13 | default=N 14 | else 15 | prompt="y/n" 16 | default= 17 | fi 18 | 19 | # Ask the question 20 | read -p "$1 [$prompt] " REPLY 21 | 22 | # Default? 23 | if [ -z "$REPLY" ]; then 24 | REPLY=$default 25 | fi 26 | 27 | # Check if the reply is valid 28 | case "$REPLY" in 29 | Y*|y*) return 0 ;; 30 | N*|n*) return 1 ;; 31 | esac 32 | 33 | done 34 | } 35 | 36 | APP="mqtt2mqtt" 37 | PKG_FOLDER="pkg" 38 | 39 | echo "Destination folder: $PKG_FOLDER" 40 | echo "App-name: $APP" 41 | 42 | VERSION=$(node -p "require('./package.json').version") 43 | echo "Version: $VERSION" 44 | 45 | NODE_MAJOR=$(node -v | egrep -o '[0-9].' | head -n 1) 46 | 47 | echo "## Clear $PKG_FOLDER folder" 48 | rm -rf $PKG_FOLDER/* 49 | 50 | 51 | if [ ! -z "$1" ]; then 52 | echo "## Building application..." 53 | echo '' 54 | npm run build 55 | echo "Executing command: pkg package.json -t node12-linux-x64 --out-path $PKG_FOLDER" 56 | pkg package.json -t node12-linux-x64 --out-path $PKG_FOLDER 57 | else 58 | 59 | if ask "Re-build $APP?"; then 60 | echo "## Building application" 61 | npm run build 62 | fi 63 | 64 | echo '###################################################' 65 | echo '## Choose architecture to build' 66 | echo '###################################################' 67 | echo ' ' 68 | echo 'Your architecture is' $(arch) 69 | PS3="Architecture: >" 70 | options=( 71 | "x64" 72 | "armv7" 73 | "armv6" 74 | "x86" 75 | "alpine" 76 | ) 77 | echo '' 78 | select option in "${options[@]}"; do 79 | case "$REPLY" in 80 | 1) 81 | echo "## Creating application package in $PKG_FOLDER folder" 82 | pkg package.json -t node$NODE_MAJOR-linux-x64 --out-path $PKG_FOLDER 83 | break 84 | ;; 85 | 2) 86 | echo "## Creating application package in $PKG_FOLDER folder" 87 | pkg package.json -t node$NODE_MAJOR-linux-armv7 --out-path $PKG_FOLDER --public-packages=* 88 | break 89 | ;; 90 | 3) 91 | echo "## Creating application package in $PKG_FOLDER folder" 92 | pkg package.json -t node$NODE_MAJOR-linux-armv6 --out-path $PKG_FOLDER --public-packages=* 93 | break 94 | ;; 95 | 4) 96 | echo "## Creating application package in $PKG_FOLDER folder" 97 | pkg package.json -t node$NODE_MAJOR-linux-x86 --out-path $PKG_FOLDER 98 | break 99 | ;; 100 | *) 101 | echo '####################' 102 | echo '## Invalid option ##' 103 | echo '####################' 104 | exit 105 | esac 106 | done 107 | fi 108 | 109 | echo "## Check for .node files to include in executable folder" 110 | mapfile -t TO_INCLUDE < <(find ./node_modules/ -type f -name "*.node" | grep -v obj.target) 111 | 112 | TOTAL_INCLUDE=${#TO_INCLUDE[@]} 113 | 114 | echo "## Found $TOTAL_INCLUDE files to include" 115 | 116 | i=0 117 | 118 | while [ "$i" -lt "$TOTAL_INCLUDE" ] 119 | do 120 | IFS='/' path=(${TO_INCLUDE[$i]}) 121 | file=${path[-1]} 122 | echo "## Copying $file to $PKG_FOLDER folder" 123 | cp "${TO_INCLUDE[$i]}" "./$PKG_FOLDER" 124 | let "i = $i + 1" 125 | done 126 | 127 | echo "## Create folders needed" 128 | cd $PKG_FOLDER 129 | mkdir store -p 130 | echo "## Create zip file $APP-v$VERSION" 131 | zip -r $APP-v$VERSION.zip * 132 | -------------------------------------------------------------------------------- /pkg/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/pkg/.gitkeep -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get('/', function(req, res, next) { 6 | res.render('index', { title: 'Express' }); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET users listing. */ 5 | router.get('/', function(req, res, next) { 6 | res.send('respond with a resource'); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 245 | -------------------------------------------------------------------------------- /src/apis/ConfigApis.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import { loadProgressBar } from 'axios-progress-bar' 4 | 5 | if (process.env.NODE_ENV === 'development') { 6 | // process.PORT is imported in config/dev.env.js 7 | axios.defaults.baseURL = location.protocol + '//' + location.hostname + ':' + process.env.PORT + '/api' 8 | } else { 9 | axios.defaults.baseURL = '/api' 10 | } 11 | 12 | loadProgressBar() 13 | 14 | export default{ 15 | getSettings () { 16 | return axios.get('/settings') 17 | .then(response => { 18 | return response.data 19 | }) 20 | }, 21 | updateSettings (data) { 22 | return axios.post('/settings', data) 23 | .then(response => { 24 | return response.data 25 | }) 26 | }, 27 | getStatus () { 28 | return axios.get('/status') 29 | .then(response => { 30 | return response.data 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/assets/css/customize.css: -------------------------------------------------------------------------------- 1 | #nprogress .spinner { 2 | right: 50% !important; 3 | } 4 | 5 | /* Custom Scrollbar */ 6 | 7 | /* Firefox */ 8 | html, 9 | body { 10 | scrollbar-color: rgba(255, 255, 255, 0.05) transparent; 11 | scrollbar-width: thin; 12 | } 13 | 14 | ::-webkit-scrollbar { 15 | height: 5px; 16 | width: 4px; 17 | background: transparent; 18 | padding-right: 10; 19 | } 20 | 21 | ::-webkit-scrollbar-thumb { 22 | background: rgba(255, 255, 255, 0.05); 23 | border-radius: 1ex; 24 | -webkit-border-radius: 1ex; 25 | } 26 | 27 | ::-webkit-scrollbar-corner { 28 | background: transparent; 29 | } 30 | 31 | /* Fix for syntax highlight (override Vuetify code props) */ 32 | 33 | .prism-editor-wrapper code { 34 | background-color: inherit; 35 | color: #ccc; 36 | -webkit-box-shadow: none; 37 | box-shadow: none; 38 | font-family: inherit; 39 | line-height: inherit; 40 | font-size: inherit; 41 | } 42 | 43 | .prism-editor-wrapper code::before { 44 | content: ""; 45 | letter-spacing: 0px; 46 | } 47 | .prism-editor-wrapper code::after { 48 | content: ""; 49 | letter-spacing: 0px; 50 | } -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Broker.vue: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | 93 | 94 | 164 | -------------------------------------------------------------------------------- /src/components/Maps.vue: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | 52 | 53 | 170 | -------------------------------------------------------------------------------- /src/components/MqttClients.vue: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | 52 | 53 | 146 | -------------------------------------------------------------------------------- /src/components/Values.vue: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | 50 | 51 | 134 | -------------------------------------------------------------------------------- /src/components/custom/file-input.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 99 | -------------------------------------------------------------------------------- /src/components/dialogs/Map.vue: -------------------------------------------------------------------------------- 1 | 180 | 181 | 255 | -------------------------------------------------------------------------------- /src/components/dialogs/Mqtt_Client.vue: -------------------------------------------------------------------------------- 1 | 192 | 193 | 285 | -------------------------------------------------------------------------------- /src/components/dialogs/Settings.vue: -------------------------------------------------------------------------------- 1 | 171 | 172 | 275 | -------------------------------------------------------------------------------- /src/components/dialogs/Value.vue: -------------------------------------------------------------------------------- 1 | 109 | 110 | 143 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import 'babel-polyfill' 4 | 5 | import Vue from 'vue' 6 | import App from './App' 7 | import router from './router' 8 | import store from './store' 9 | 10 | import Vuetify from 'vuetify' 11 | import 'vuetify/dist/vuetify.min.css' 12 | 13 | import 'axios-progress-bar/dist/nprogress.css' 14 | 15 | // Custom assets CSS JS 16 | require('./assets/css/customize.css') 17 | 18 | Vue.use(Vuetify) 19 | 20 | Vue.config.productionTip = false 21 | Vue.config.devtools = process.env.NODE_ENV === 'development' 22 | 23 | /* eslint-disable no-new */ 24 | new Vue({ 25 | el: '#app', 26 | router, 27 | store, 28 | components: { App }, 29 | template: '' 30 | }) 31 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import MqttClients from '@/components/MqttClients' 4 | import Broker from '@/components/Broker' 5 | import Maps from '@/components/Maps' 6 | import Values from '@/components/Values' 7 | 8 | Vue.use(Router) 9 | 10 | export default new Router({ 11 | mode: 'history', 12 | routes: [ 13 | { 14 | path: '/', 15 | name: 'Broker', 16 | component: Broker, 17 | props: true 18 | }, 19 | { 20 | path: '/values', 21 | name: 'Values', 22 | component: Values, 23 | props: true 24 | }, 25 | { 26 | path: '/clients', 27 | name: 'Mqtt Clients', 28 | component: MqttClients, 29 | props: true 30 | }, 31 | { 32 | path: '/maps', 33 | name: 'Maps', 34 | component: Maps, 35 | props: true 36 | } 37 | ] 38 | }) 39 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import { state, mutations, getters, actions } from './mutations' 4 | 5 | Vue.use(Vuex) 6 | 7 | export default new Vuex.Store({ 8 | state, 9 | mutations, 10 | getters, 11 | actions 12 | }) 13 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | export const state = { 2 | clients: [], 3 | values: [], 4 | maps: [], 5 | broker: {} 6 | } 7 | 8 | export const getters = { 9 | clients: state => state.clients, 10 | values: state => state.values, 11 | maps: state => state.maps, 12 | broker: state => state.broker, 13 | configuration: state => JSON.parse(JSON.stringify(state)) 14 | } 15 | 16 | export const actions = { 17 | init (store, data) { 18 | if(data){ 19 | store.commit('initSettings', data); 20 | } 21 | }, 22 | updateBroker(store, data){ 23 | store.commit('updateBroker', data); 24 | } 25 | } 26 | 27 | export const mutations = { 28 | initSettings(state, data){ 29 | // JSON Configuration version 30 | state.version = 1; 31 | 32 | state.clients = data.clients || []; 33 | state.values = data.values || []; 34 | state.maps = data.maps || []; 35 | state.broker = data.broker || {}; 36 | }, 37 | updateBroker(state, data){ 38 | state.broker = data; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/static/.gitkeep -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/static/favicon.ico -------------------------------------------------------------------------------- /static/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/static/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/favicons/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/static/favicons/android-chrome-384x384.png -------------------------------------------------------------------------------- /static/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/static/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /static/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #603cba 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/static/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/static/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/static/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /static/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 19 | 22 | 28 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /static/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mqtt2Mqtt", 3 | "short_name": "Mqtt2Mqtt", 4 | "icons": [ 5 | { 6 | "src": "/static/favicons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/static/favicons/android-chrome-384x384.png", 12 | "sizes": "384x384", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/static/logo.png -------------------------------------------------------------------------------- /store/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertsLando/Mqtt2Mqtt/f6de4fae6fce60cf46a9fb3859c57eb95ccd6a35/store/.gitkeep -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 |

<%= message %>

2 |

<%= error.status %>

3 |
<%= error.stack %>
4 | --------------------------------------------------------------------------------