diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..932b7a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history +dynamicFolderStructure.json + +# Idea +.idea diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a5ec628 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Project Mi5 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/example/config.js b/example/config.js new file mode 100644 index 0000000..c3c010a --- /dev/null +++ b/example/config.js @@ -0,0 +1,70 @@ +var ModuleID = '1202'; + +var folderStructure = require('./folderStructure.json'); + +var helper = require('mi5-simple-opcua').helper; +var expandFolderStructure = helper.expandFolderStructure; +var setInitValues = helper.setInitValues; + +folderStructure = expandFolderStructure(folderStructure); + +exports.mqtt = { + host: 'tcp://mi5.itq.de', + topics: { + skill1: '/sensor_input', + skill2: '/sensor_output' + } +}; + +var valueStatements = [ +{ + path: 'Output.Name', + initValue: 'Check Module' +}, +{ + path: 'Output.ID', + initValue: ModuleID +}, +{ + path: 'Output.SkillOutput.SkillOutput0.Dummy', + initValue: false +}, +{ + path: 'Output.SkillOutput.SkillOutput0.ID', + initValue: 5010 +}, +{ + path: 'Output.SkillOutput.SkillOutput1.Dummy', + initValue: false +}, +{ + path: 'Output.SkillOutput.SkillOutput1.ID', + initValue: 5011 +} +]; + +folderStructure = setInitValues(folderStructure, valueStatements); + +var ServerStructure = { + moduleName: 'Module'+ModuleID, + serverInfo: { + port: 4842, // the port of the listening socket of the server + resourcePath: "", // this path will be added to the endpoint resource name + buildInfo : { + productName: 'Module'+ModuleID, //module name + buildNumber: "7658", + buildDate: new Date(2016,3,25) + } + }, + rootFolder: "RootFolder", + baseNodeId: "ns=4;s=MI5.", + content:{} + +}; + +ServerStructure.content['Module'+ModuleID] = { + type: 'Folder', + content: folderStructure +}; + +exports.ServerStructure = ServerStructure; diff --git a/example/example.js b/example/example.js new file mode 100644 index 0000000..551e0fb --- /dev/null +++ b/example/example.js @@ -0,0 +1,22 @@ +/** + * Created by Dominik on 15.06.2016. + */ + +var Mi5Module = require('mi5-module').Mi5Module; +var config = require('./config'); + +var moduleSettings = { + mqtt: { + hostAddress: config.mqtt.host + }, + opcua: { + server: config.ServerStructure + } +}; + +var TestModule = new Mi5Module('test module', moduleSettings); +TestModule.on('connect', function(){ + console.log('received event.'); + var Mi5Skill = require('mi5-module').Mi5Skill; + var TestSkill = new Mi5Skill(0, 'TestSkill', TestModule); +}); diff --git a/example/folderStructure.json b/example/folderStructure.json new file mode 100644 index 0000000..e21d812 --- /dev/null +++ b/example/folderStructure.json @@ -0,0 +1,587 @@ +{ + "Input": { + "type": "Folder", + "content": { + "ConnectionTestInput": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "EmergencyStop": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "Mode": { + "type": "Variable", + "dataType": "Int16", + "initValue": 1 + }, + "PositionInput": { + "type": "Variable", + "dataType": "Double", + "initValue": 3000 + }, + "SkillInput": { + "type": "Folder", + "content": { + "SkillInput#index1": { + "type": "Folder", + "content": { + "Execute": { + "type": "Variable", + "dataType": "Boolean", + "initValue": "false" + }, + "ParameterInput": { + "type": "Folder", + "content": { + "ParameterInput": { + "numberOfRepetitions": 6, + "repeat": true, + "type": "Folder", + "content": { + "StringValue": { + "type": "Variable", + "dataType": "String", + "initValue": "" + }, + "Value": { + "type": "Variable", + "dataType": "Double", + "initValue": "0" + } + } + } + } + } + }, + "repeat": true, + "numberOfRepetitions": 16, + "indexKey": "#index1" + } + } + }, + "Watchbit": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + } + } + }, + "Output": { + "type": "Folder", + "content": { + "Connected": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "ConnectionTestOutput": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "CurrentTaskDescription": { + "type": "Variable", + "dataType": "String", + "initValue": "dummy task description" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "Error": { + "type": "Variable", + "dataType": "Boolean", + "initValue": "false" + }, + "ErrorDescription": { + "type": "Variable", + "dataType": "String", + "initValue": "" + }, + "ErrorId": { + "type": "Variable", + "dataType": "UInt16", + "initValue": "0" + }, + "ID": { + "type": "Variable", + "dataType": "UInt16", + "initValue": "0" + }, + "IP": { + "type": "Variable", + "dataType": "String", + "initValue": "" + }, + "Idle": { + "type": "Variable", + "dataType": "Boolean", + "initValue": "false" + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "Cookie Module (Mock)" + }, + "PositionOutput": { + "type": "Variable", + "dataType": "Double", + "initValue": "3750" + }, + "PositionSensor": { + "type": "Variable", + "dataType": "Boolean", + "initValue": "false" + }, + "SkillOutput": { + "type": "Folder", + "content": { + "SkillOutput#index2": { + "type": "Folder", + "content": { + "Activated": { + "type": "Variable", + "dataType": "Boolean", + "initValue": "true" + }, + "Busy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": "false" + }, + "Done": { + "type": "Variable", + "dataType": "Boolean", + "initValue": "false" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": "true" + }, + "Error": { + "type": "Variable", + "dataType": "Boolean", + "initValue": "false" + }, + "ID": { + "type": "Variable", + "dataType": "Double", + "initValue": "" + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "" + }, + "ParameterOutput": { + "type": "Folder", + "content": { + "ParameterOutput": { + "numberOfRepetitions": 6, + "repeat": true, + "type": "Folder", + "content": { + "Default": { + "type": "Variable", + "dataType": "Double", + "initValue": "0" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": "true" + }, + "ID": { + "type": "Variable", + "dataType": "UInt16", + "initValue": "0" + }, + "MaxValue": { + "type": "Variable", + "dataType": "Double", + "initValue": "0" + }, + "MinValue": { + "type": "Variable", + "dataType": "Double", + "initValue": "0" + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "" + }, + "Required": { + "type": "Variable", + "dataType": "Boolean", + "initValue": "false" + }, + "Unit": { + "type": "Variable", + "dataType": "String", + "initValue": "" + } + } + } + } + }, + "Ready": { + "type": "Variable", + "dataType": "Boolean", + "initValue": "true" + } + }, + "repeat": true, + "numberOfRepetitions": 16, + "indexKey": "#index2" + } + } + }, + "StateValue": { + "type": "Folder", + "content": { + "StateValue0": { + "type": "Folder", + "content": { + "Description": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream in Module" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream" + }, + "Unit": { + "type": "Variable", + "dataType": "String", + "initValue": "ml" + }, + "Value": { + "type": "Variable", + "dataType": "Double", + "initValue": 123 + } + } + }, + "StateValue1": { + "type": "Folder", + "content": { + "Description": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream in Module" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream" + }, + "Unit": { + "type": "Variable", + "dataType": "String", + "initValue": "ml" + }, + "Value": { + "type": "Variable", + "dataType": "Double", + "initValue": 123 + } + } + }, + "StateValue2": { + "type": "Folder", + "content": { + "Description": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream in Module" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream" + }, + "Unit": { + "type": "Variable", + "dataType": "String", + "initValue": "ml" + }, + "Value": { + "type": "Variable", + "dataType": "Double", + "initValue": 123 + } + } + }, + "StateValue3": { + "type": "Folder", + "content": { + "Description": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream in Module" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream" + }, + "Unit": { + "type": "Variable", + "dataType": "String", + "initValue": "ml" + }, + "Value": { + "type": "Variable", + "dataType": "Double", + "initValue": 123 + } + } + }, + "StateValue4": { + "type": "Folder", + "content": { + "Description": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream in Module" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream" + }, + "Unit": { + "type": "Variable", + "dataType": "String", + "initValue": "ml" + }, + "Value": { + "type": "Variable", + "dataType": "Double", + "initValue": 123 + } + } + }, + "StateValue5": { + "type": "Folder", + "content": { + "Description": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream in Module" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream" + }, + "Unit": { + "type": "Variable", + "dataType": "String", + "initValue": "ml" + }, + "Value": { + "type": "Variable", + "dataType": "Double", + "initValue": 123 + } + } + }, + "StateValue6": { + "type": "Folder", + "content": { + "Description": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream in Module" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream" + }, + "Unit": { + "type": "Variable", + "dataType": "String", + "initValue": "ml" + }, + "Value": { + "type": "Variable", + "dataType": "Double", + "initValue": 123 + } + } + }, + "StateValue7": { + "type": "Folder", + "content": { + "Description": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream in Module" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream" + }, + "Unit": { + "type": "Variable", + "dataType": "String", + "initValue": "ml" + }, + "Value": { + "type": "Variable", + "dataType": "Double", + "initValue": 123 + } + } + }, + "StateValue8": { + "type": "Folder", + "content": { + "Description": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream in Module" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream" + }, + "Unit": { + "type": "Variable", + "dataType": "String", + "initValue": "ml" + }, + "Value": { + "type": "Variable", + "dataType": "Double", + "initValue": 123 + } + } + }, + "StateValue9": { + "type": "Folder", + "content": { + "Description": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream in Module" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream" + }, + "Unit": { + "type": "Variable", + "dataType": "String", + "initValue": "ml" + }, + "Value": { + "type": "Variable", + "dataType": "Double", + "initValue": 123 + } + } + }, + "StateValue10": { + "type": "Folder", + "content": { + "Description": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream in Module" + }, + "Dummy": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + }, + "Name": { + "type": "Variable", + "dataType": "String", + "initValue": "Remaining Cream" + }, + "Unit": { + "type": "Variable", + "dataType": "String", + "initValue": "ml" + }, + "Value": { + "type": "Variable", + "dataType": "Double", + "initValue": 123 + } + } + } + } + } + } + }, + "reset": { + "type": "Variable", + "dataType": "Boolean", + "initValue": false + } +} \ No newline at end of file diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..284ddce --- /dev/null +++ b/example/package.json @@ -0,0 +1,14 @@ +{ + "name": "mi5-module-example", + "version": "0.0.1", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "dominik serve", + "license": "MIT", + "dependencies": { + "mi5-module-js": "git+https://github.com/ProjectMi5/mi5-module-js.git", + } +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..8de36c9 --- /dev/null +++ b/index.js @@ -0,0 +1,2 @@ +module.exports.Mi5Module = require('./models/mi5-module'); +module.exports.Mi5Skill = require('./models/mi5-skill'); \ No newline at end of file diff --git a/models/mi5-module.js b/models/mi5-module.js new file mode 100644 index 0000000..7111b83 --- /dev/null +++ b/models/mi5-module.js @@ -0,0 +1,102 @@ +/** + * Created by Dominik Serve on 15.06.2016. + */ + +var simpleOpcua = require('mi5-simple-opcua'); +var mqtt = require('mqtt'); +var EventEmitter = require('events').EventEmitter; +var util = require('util'); +util.inherits(Mi5Module, EventEmitter); +var connectionCount = 0; + +var OpcuaServer = simpleOpcua.OpcuaServer; +var OpcuaClient = simpleOpcua.OpcuaClient; +//var OpcuaVariable = simpleOpcua.OpcuaVariable; + + +/**Mi5 Module inherits EventEmitter + * + * @param moduleName + * @param {object} settings {opcua: {hostAddress: ..., baseNodeId: ...}, mqtt: ..., indPhysx: ...} + * @constructor + */ + +function Mi5Module(trivialName, settings){ + console.log('creating new module '+trivialName); + EventEmitter.call(this); + var self = this; + + this.trivialName = trivialName; + // opcua + this.baseNodeId = settings.opcua.baseNodeId; + this.opcuaSettings = settings.opcua; + this.opcuaServer = createOpcuaServer(); + this.opcuaClient = connectToOpcuaServer(); + // mqtt + this.mqttSettings = settings.mqtt; + this.mqttClient = connectToMQTTClient(); + // indPhysx + this.indPhysxSettings = settings.indPhysx; + + + // functions + + function createOpcuaServer(){ + var opcuaSettings = self.opcuaSettings; + if(!opcuaSettings){ + return null; + } + if(!opcuaSettings.server){ + return null; + } + // creating host ip + opcuaSettings.hostAddress = "opc.tcp://" + require("os").hostname() + ":" + opcuaSettings.server.serverInfo.port; + self.baseNodeId = opcuaSettings.server.baseNodeId; + self.moduleName = opcuaSettings.server.moduleName; + // starting server + return OpcuaServer.newOpcuaServer(opcuaSettings.server); + } + + function connectToOpcuaServer(){ + var opcuaSettings = self.opcuaSettings; + console.log('connect to opcua server '+JSON.stringify(opcuaSettings.hostAddress)); + if(!opcuaSettings){ + return null; + } + connectionCount++; + return new OpcuaClient(opcuaSettings.hostAddress, function(err){ + if(!err) + newConnectionEstablished(); + }); + } + + function connectToMQTTClient(){ + var mqttSettings = self.mqttSettings; + console.log('connect to mqtt broker '+JSON.stringify(mqttSettings)); + if(!mqttSettings){ + return null; + } + connectionCount++; + var mqttClient = mqtt.connect(mqttSettings.hostAddress); + mqttClient.on('connect', function(){ + console.log('connected to mqtt broker.'); + newConnectionEstablished(); + }); + mqttClient.on('error', console.log); + return mqttClient; + } + + function newConnectionEstablished(){ + connectionCount--; + if(connectionCount == 0){ + console.log('All connections established successfully.'); + self.emit('connect'); + } + } + +} + + + + +module.exports = Mi5Module; \ No newline at end of file diff --git a/models/mi5-skill.js b/models/mi5-skill.js new file mode 100644 index 0000000..285cc20 --- /dev/null +++ b/models/mi5-skill.js @@ -0,0 +1,110 @@ +var OpcuaVariable = require('mi5-simple-opcua').OpcuaVariable; + +var EventEmitter = require('events').EventEmitter; +var util = require('util'); +util.inherits(Skill, EventEmitter); + +function Skill(SkillNumber, SkillName, Mi5Module){ + console.log('Skill init '+ SkillNumber + ' ' + SkillName); + EventEmitter.call(this); + var self = this; + this.skillNumber = SkillNumber; + this.skillName = SkillName; + + var endOfBaseNodeId = Mi5Module.baseNodeId.split('').pop(); + var dot = '.'; + if(endOfBaseNodeId === '.'){ + dot = ''; + } + + var baseNodeIdInput = Mi5Module.baseNodeId + dot + Mi5Module.moduleName + '.Input.SkillInput.SkillInput' + SkillNumber + '.'; + var baseNodeIdOutput = Mi5Module.baseNodeId + dot + Mi5Module.moduleName + '.Output.SkillOutput.SkillOutput' + SkillNumber + '.'; + + this.execute = new OpcuaVariable(Mi5Module.opcuaClient, baseNodeIdInput + 'Execute'); + this.ready = new OpcuaVariable(Mi5Module.opcuaClient, baseNodeIdOutput + 'Ready'); + this.busy = new OpcuaVariable(Mi5Module.opcuaClient, baseNodeIdOutput + 'Busy'); + this.done = new OpcuaVariable(Mi5Module.opcuaClient, baseNodeIdOutput + 'Done'); + this.error = new OpcuaVariable(Mi5Module.opcuaClient, baseNodeIdOutput + 'Error'); + + + this.execute.onChange(function(value){ + console.log('Skill'+self.skillNumber+', '+self.skillName+': execute ' + value); + if(value){ + self.setBusy(); + setTimeout(function(){ + self.finishTask(); + },2000); + setTimeout(function(){ + self.setDone() + },3000); + setTimeout(function(){ + self.setReady() + },4000); + } + }); + + this.error.onChange(function(value){ + console.log('Skill'+self.skillNumber+', '+self.skillName+': error ' + value); + self.setError(value); + }); + + + +} + + +Skill.prototype.setBusy = function(){ + var self = this; + console.log('Skill'+self.skillNumber+', '+self.skillName+': set busy.'); + self.done.write(false); + self.busy.write(true); + self.ready.write(false); + /*writeToIndPhysix("status_self.busy","TRUE"); + writeToIndPhysix("status_self.ready","FALSE"); + writeToIndPhysix("status_self.done","FALSE");*/ +}; + +Skill.prototype.finishTask = function(){ + var self = this; + console.log('Skill'+self.skillNumber+', '+self.skillName+': finished its task.'); + self.busy.write(false); + //writeToIndPhysix("status_self.busy","FALSE"); +}; + +Skill.prototype.setDone = function(){ + var self = this; + console.log('Skill'+self.skillNumber+', '+self.skillName+': set done.'); + self.done.write(true); + //writeToIndPhysix("status_self.done","TRUE"); +}; + +Skill.prototype.setReady = function(){ + var self = this; + console.log('Skill'+self.skillNumber+', '+self.skillName+': set ready.'); + self.busy.write(false); + self.ready.write(true); + self.done.write(false); + /*writeToIndPhysix("status_self.busy","FALSE"); + writeToIndPhysix("status_self.ready","TRUE"); + writeToIndPhysix("status_self.done","FALSE");*/ +}; + +Skill.prototype.setError = function(value){ + var self = this; + if(value){ + //writeToIndPhysix("status_self.error","TRUE"); + } + else { + //writeToIndPhysix("status_self.error","FALSE"); + } +}; + +/*function writeToIndPhysix(variableName,value){ + // SkillNumber+1 equals pump number for Cocktail Module + var outputString = 'setIOValue("' + indPhysxSkillPath + '","' + variableName + '","' + value + '");'; + Mi5Module.indPhysxClient.write(outputString); + //console.log(outputString); +}*/ + +module.exports = Skill; + diff --git a/package.json b/package.json new file mode 100644 index 0000000..a3fe585 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "mi5-module", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "dominik serve", + "license": "MIT", + "dependencies": { + "mi5-simple-opcua": "git+https://github.com/ProjectMi5/mi5-simple-opcua.git", + "mqtt": "^1.11.0" + }, + "directories": { + "example": "example", + "models": "models" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ProjectMi5/mi5-module-js.git" + }, + "bugs": { + "url": "https://github.com/ProjectMi5/mi5-module-js/issues" + }, + "homepage": "https://github.com/ProjectMi5/mi5-module-js#readme", + "description": "" +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..44faeeb --- /dev/null +++ b/readme.md @@ -0,0 +1,8 @@ +# mi5 module js + +This app helps modeling mi5 modules backend. More information coming soon. + +## Getting Started + +* coming soon. +