├── README.md ├── package.json ├── service ├── common │ ├── messageModel.js │ └── mqttClient.js ├── opcua │ └── opcClient.js └── simulator │ └── simulator.js ├── src ├── index.html ├── index.js ├── login │ ├── index.html │ ├── index.js │ └── login.css └── main │ ├── index.css │ ├── index.html │ └── index.js ├── user.js └── user └── user.js /README.md: -------------------------------------------------------------------------------- 1 | ezutai(易组态) 2 | ========== 3 | 4 | 一款易上手、易通讯、易扩展的web组态,可用于工业云组态 5 | 6 | ###特色 7 | 8 | 快速搭建界面模型,简单方便地与设备建立链接,可以容易搭建出你心目中组态的构图~,ezutai本身提供模拟器,可以在没有设备链接的情况下,模拟出设备运行状态,同时,我们也提供了OPC-UA的连接模块,直接与设备建立链接实时通讯。 9 | 10 | 11 | ## 使用 12 | 13 | ### 使用之前 14 | 15 | 在浏览器中 打开 [官网](http://www.senkins.top/) 或者 [系统平台](http://ezutai.senkins.top/) 16 | 17 | 申请一个免费的用户账号,创建组态并运行,模拟器环境下开关量的通讯参数写入bool 18 | #### 19 | 20 | #### 启用 ezutai(易组态) 21 | 22 | $ git clone https://github.com/sekikyo/ezutaiSDK.git 23 | $ cd ezutaiSDK 24 | $ npm install 25 | $ npm start 26 | 27 | #### 28 | 系统平台 29 | ### 简易教程 30 | 31 | 32 | * 运行易组态,登录账号 33 | 34 | * 登录主界面,选择连接方式,(目前:我们仅提供[模拟器]和[OPCUA]2种连接方式,其他通讯方式可自行扩展) 35 | 36 | * 选择方式(默认模拟器),然后点击启动。 37 | 38 | 直接下载各平台可执行程序[mac os](http://ezutai.senkins.top/download/ezutai_mac.zip/),[win64](http://ezutai.senkins.top/download/ezutai_win64.exe/) 39 | 40 | ## 其他通讯自定义扩展 41 | 如果您喜欢我们的工具,我们也支持其他通讯连接的扩展 42 | 43 | * 在 server 目录下 新建 demo 文件夹 ,新建 demo.js 44 | 45 | * 必须引入 require("../common/messageModel") 46 | 47 | * 必须继承 messageModel.initCon 48 | 49 | 50 | * 在Class demo 中必须有 function onReceiveMqttMessage() 51 | 52 | ## Example 53 | 54 | ```javascript 55 | 56 | const messageModel = require("../common/messageModel") 57 | 58 | class demo extends messageModel{ 59 | constructor(option){ 60 | super(); 61 | super.initCon(option,this.onReceiveMqttMessage); 62 | } 63 | disconnect(){ 64 | super.disconnect() 65 | } 66 | async onReceiveMqttMessage(msg){ 67 | 68 | } 69 | 70 | } 71 | 72 | ``` 73 | 74 | ## Getting support 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ezutai", 3 | "productName": "ezutai", 4 | "version": "1.0.0", 5 | "description": "My Electron application description", 6 | "main": "src/index.js", 7 | "scripts": { 8 | "start": "electron-forge start", 9 | "package": "electron-forge package", 10 | "make": "electron-forge make", 11 | "publish": "electron-forge publish", 12 | "lint": "eslint src --color" 13 | }, 14 | "keywords": [], 15 | "author": "jingshi", 16 | "license": "MIT", 17 | "config": { 18 | "forge": { 19 | "make_targets": { 20 | "win32": [ 21 | "squirrel" 22 | ], 23 | "darwin": [ 24 | "zip" 25 | ], 26 | "linux": [ 27 | "deb", 28 | "rpm" 29 | ] 30 | }, 31 | "electronPackagerConfig": { 32 | "packageManager": "yarn" 33 | }, 34 | "electronWinstallerConfig": { 35 | "name": "ezutai" 36 | }, 37 | "electronInstallerDebian": {}, 38 | "electronInstallerRedhat": {}, 39 | "github_repository": { 40 | "owner": "", 41 | "name": "" 42 | }, 43 | "windowsStoreConfig": { 44 | "packageName": "", 45 | "name": "ezutai" 46 | } 47 | } 48 | }, 49 | "dependencies": { 50 | "async-mqtt": "^1.0.1", 51 | "chalk": "^2.4.1", 52 | "electron-compile": "^6.4.2", 53 | "electron-squirrel-startup": "^1.0.0", 54 | "fs": "^0.0.1-security", 55 | "lodash": "^4.17.10", 56 | "node-fetch": "^2.1.2", 57 | "node-opcua": "^0.3.0", 58 | "path": "^0.12.7" 59 | }, 60 | "devDependencies": { 61 | "babel-plugin-transform-async-to-generator": "^6.24.1", 62 | "babel-preset-env": "^1.6.1", 63 | "babel-preset-react": "^6.24.1", 64 | "electron-forge": "^5.2.1", 65 | "electron-packager": "^12.0.2", 66 | "electron-prebuilt-compile": "2.0.0", 67 | "eslint": "^3", 68 | "eslint-config-airbnb": "^15", 69 | "eslint-plugin-import": "^2", 70 | "eslint-plugin-jsx-a11y": "^5", 71 | "eslint-plugin-react": "^7" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /service/common/messageModel.js: -------------------------------------------------------------------------------- 1 | import { log } from 'util'; 2 | 3 | const mqttClient = require('./mqttClient'); 4 | const {ipcRenderer} = require('electron'); 5 | var numcount = 0; 6 | var wc = null 7 | 8 | /* 9 | 通讯基类 10 | */ 11 | class messageModel { 12 | constructor(){ 13 | } 14 | async initCon(option,fn){ 15 | //连接消息服务器 16 | const mqtt = await mqttClient.connectMqtt(option); 17 | //开始监听消息 18 | mqtt.on('message', async (topic, message) => { 19 | numcount++; 20 | message = message.toString() 21 | const base64 = new Buffer(message, 'base64') 22 | const jsonStr = base64.toString(); 23 | 24 | try { 25 | const msgJson = JSON.parse(jsonStr); 26 | const responseBody = await fn(msgJson) 27 | // 组装 28 | let ret = { 29 | id: numcount, 30 | msg: msgJson.type, 31 | date: new Date().toLocaleString(), 32 | value: responseBody 33 | } 34 | const res = new Buffer(JSON.stringify(ret)) 35 | //发送 36 | await mqtt.publish('gs_' + msgJson.topic, res.toString('base64'), { qos: 1 }) 37 | //显示消息 38 | wc.webContents.send("print-msg", ret) 39 | 40 | } catch (e) { 41 | 42 | console.log('Failed message:'+ e); 43 | return; 44 | } 45 | }) 46 | 47 | return true; 48 | 49 | 50 | } 51 | disconnect(){ 52 | // 断开消息服务器连接 53 | mqttClient.closeMqtt().then((res)=>{ 54 | if (res) { 55 | return true; 56 | } else { 57 | return false; 58 | } 59 | }); 60 | 61 | } 62 | //主进程 63 | initWc(_wc){ 64 | wc = _wc 65 | } 66 | 67 | } 68 | 69 | 70 | 71 | module.exports = messageModel; -------------------------------------------------------------------------------- /service/common/mqttClient.js: -------------------------------------------------------------------------------- 1 | var MQTT = require("async-mqtt") 2 | const fs = require('fs') 3 | const path = require('path') 4 | const mqttUrl = 'mqtt://47.93.205.223:1883'; 5 | 6 | var mClient; 7 | const mqttclient = { 8 | async init(topic) { 9 | var client = await MQTT.connect(mqttUrl); 10 | mClient = client; 11 | return new Promise(async (resolve) => { 12 | 13 | await client.subscribe('ur_' + topic, { qos: 1 }); 14 | 15 | if (client._client.connected) { 16 | return resolve(client); 17 | } else { 18 | return resolve(false); 19 | } 20 | }); 21 | }, 22 | /** 23 | * 连接mqtt服务器 24 | */ 25 | async connectMqtt(option) { 26 | const mqtt = await this.init(option); 27 | 28 | if (!mqtt) { 29 | 30 | return false 31 | } 32 | 33 | return mqtt 34 | 35 | }, 36 | async closeMqtt(option) { 37 | 38 | if (mClient == null) { 39 | 40 | return false; 41 | 42 | } 43 | 44 | mClient.end(); 45 | 46 | return true 47 | 48 | } 49 | 50 | 51 | } 52 | 53 | 54 | module.exports = mqttclient; -------------------------------------------------------------------------------- /service/opcua/opcClient.js: -------------------------------------------------------------------------------- 1 | const messageModel = require("../common/messageModel") 2 | const opcua = require("node-opcua"); 3 | 4 | const endpointUrl = "opc.tcp://192.168.0.18:49320"; 5 | const OPCUAClient = opcua.OPCUAClient; 6 | const SecurityPolicy = opcua.SecurityPolicy; 7 | const MessageSecurityMode = opcua.MessageSecurityMode; 8 | const client = new OPCUAClient({ 9 | securityMode: MessageSecurityMode.SIGNANDENCRYPT, 10 | securityPolicy: SecurityPolicy.Basic128Rsa15 11 | }); 12 | var the_session; 13 | 14 | const retVariable =(nodeId)=>{ 15 | return new Promise((resolve, reject) => { 16 | setTimeout(()=>{ 17 | return resolve(0) 18 | },1500); 19 | the_session.readVariableValue(nodeId, function (err, dataValue) { 20 | if (!err) { 21 | if (dataValue.value == null) { 22 | return resolve(0) 23 | } 24 | if ('Float' === dataValue.value.dataType.key) { 25 | dataValue.value.value = parseFloat(dataValue.value.value.toFixed(2)) 26 | } 27 | let msgValue = dataValue.value.value; 28 | return resolve(msgValue) 29 | } 30 | return reject(err) 31 | }); 32 | }) 33 | } 34 | 35 | class opcClient extends messageModel{ 36 | constructor(option){ 37 | super(); 38 | this.opcUAConnect().then((res) => { 39 | if (res) { 40 | this.opcSession() 41 | super.initCon(option, this.onReceiveMqttMessage); 42 | } else { 43 | return false; 44 | } 45 | 46 | }); 47 | 48 | } 49 | disconnect(){ 50 | //消息服务器断开连接 51 | super.disconnect() 52 | //关闭连接 53 | client.disconnect().then(); 54 | 55 | } 56 | async opcUAConnect() { 57 | return new Promise((resolve, reject) => { 58 | client.connect(endpointUrl, function (err) { 59 | if (err) { 60 | console.log(" cannot connect to endpoint :", endpointUrl); 61 | reject(err) 62 | } else { 63 | resolve(true) 64 | } 65 | }); 66 | }) 67 | } 68 | async opcSession() { 69 | return new Promise((resolve, reject) => { 70 | client.createSession(function (err, session) { 71 | if (!err) { 72 | the_session = session; 73 | return resolve(the_session) 74 | } 75 | console.log(err) 76 | return reject(err) 77 | }); 78 | }); 79 | } 80 | async onReceiveMqttMessage(message) { 81 | let ret; 82 | try{ 83 | 84 | ret = await retVariable(message.tid); 85 | 86 | return ret; 87 | 88 | }catch (e){ 89 | 90 | console.log("Failed to Send msg:"+ e); 91 | 92 | return false 93 | } 94 | } 95 | } 96 | 97 | module.exports = opcClient -------------------------------------------------------------------------------- /service/simulator/simulator.js: -------------------------------------------------------------------------------- 1 | const messageModel = require("../common/messageModel") 2 | 3 | 4 | class simulator extends messageModel{ 5 | constructor(option){ 6 | super(); 7 | super.initCon(option,this.onReceiveMqttMessage); 8 | } 9 | disconnect(){ 10 | super.disconnect() 11 | } 12 | async onReceiveMqttMessage(msg) { 13 | if (msg.type == 'bool') { 14 | if(Math.random()>0.5){ 15 | return 1 16 | }else{ 17 | return 0 18 | } 19 | } else { 20 | return Math.round(Math.random() * 10000) / 100; 21 | } 22 | 23 | } 24 | 25 | } 26 | module.exports = simulator -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Well hey there!!! 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain } from 'electron'; 2 | const user = require('../user/user'); 3 | const simulator = require('../service/simulator/simulator'); 4 | const opcClient = require('../service/opcua/opcClient'); 5 | 6 | if (require('electron-squirrel-startup')) { 7 | app.quit(); 8 | } 9 | 10 | 11 | let mainWindow; 12 | 13 | const createWindow = () => { 14 | 15 | mainWindow = new BrowserWindow({ 16 | width: 800, 17 | height: 600, 18 | }); 19 | 20 | 21 | mainWindow.loadURL(`file://${__dirname}/login/index.html`); 22 | 23 | 24 | 25 | mainWindow.on('closed', () => { 26 | 27 | mainWindow = null; 28 | }); 29 | }; 30 | 31 | 32 | app.on('ready', createWindow); 33 | 34 | 35 | app.on('window-all-closed', () => { 36 | 37 | if (process.platform !== 'darwin') { 38 | app.quit(); 39 | } 40 | }); 41 | 42 | app.on('activate', () => { 43 | 44 | if (mainWindow === null) { 45 | createWindow(); 46 | } 47 | }); 48 | // 用户信息 49 | let userInfo; 50 | const login = async (event, phoneno, password) => { 51 | 52 | userInfo = await user.sdklogin(phoneno, password); 53 | 54 | event.returnValue = userInfo; 55 | } 56 | 57 | const loginOut = async (event, arg) => { 58 | 59 | userInfo = null; 60 | 61 | event.returnValue = arg; 62 | } 63 | 64 | const showNotice = async (event, msg) => { 65 | 66 | mainWindow.webContents.send("send-msg", msg) 67 | return event.returnValue = 1 68 | 69 | } 70 | 71 | let client; 72 | const openServer = async (event, arg) => { 73 | let clientType; 74 | switch (arg) { 75 | case 0: 76 | clientType = simulator 77 | break 78 | case 1: 79 | clientType = opcClient 80 | break; 81 | 82 | default: 83 | clientType = simulator 84 | break; 85 | } 86 | 87 | client = new clientType(userInfo.topic) 88 | client.initWc(mainWindow); 89 | event.returnValue = arg; 90 | } 91 | 92 | const closeServer = async (event, arg) => { 93 | 94 | let temp = client.disconnect(); 95 | 96 | client = null; 97 | 98 | event.returnValue = arg; 99 | 100 | } 101 | 102 | 103 | const iSMef = { 104 | "login": login, 105 | "open-server": openServer, 106 | "close-server": closeServer, 107 | "show-notice": showNotice, 108 | "login-out":loginOut 109 | } 110 | // 此处初始化所有消息 一般情况下无需修改 111 | for (var item in iSMef) { 112 | ipcMain.on(item, iSMef[item]) 113 | } 114 | 115 | -------------------------------------------------------------------------------- /src/login/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |

易组态

12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/login/index.js: -------------------------------------------------------------------------------- 1 | const {ipcRenderer} = require('electron'); 2 | const crypto = require('crypto'); 3 | 4 | 5 | function login(){ 6 | let phoneNo = document.getElementById("phoneno").value; 7 | let password = document.getElementById("password").value; 8 | 9 | const md5 =crypto.createHash("md5"); 10 | 11 | let newpwd = md5.update(password + "skkjezutai").digest("hex") 12 | 13 | 14 | var temp = ipcRenderer.sendSync('login', phoneNo, newpwd); 15 | if (temp) { 16 | window.location.href = "../main/index.html"; 17 | } else { 18 | alert("用户名或密码错误~"); 19 | } 20 | 21 | 22 | } -------------------------------------------------------------------------------- /src/login/login.css: -------------------------------------------------------------------------------- 1 | html{ 2 | width: 100%; 3 | height: 100%; 4 | overflow: hidden; 5 | font-style: sans-serif; 6 | } 7 | body{ 8 | width: 100%; 9 | height: 100%; 10 | font-family: 'Open Sans',sans-serif; 11 | margin: 0; 12 | background-color: #4A374A; 13 | } 14 | #login{ 15 | position: absolute; 16 | top: 50%; 17 | left:50%; 18 | margin: -150px 0 0 -150px; 19 | width: 300px; 20 | height: 300px; 21 | } 22 | #login h1{ 23 | color: #fff; 24 | text-shadow:0 0 10px; 25 | letter-spacing: 1px; 26 | text-align: center; 27 | } 28 | h1{ 29 | font-size: 2em; 30 | margin: 0.67em 0; 31 | } 32 | input{ 33 | width: 278px; 34 | height: 18px; 35 | margin-bottom: 10px; 36 | outline: none; 37 | padding: 10px; 38 | font-size: 13px; 39 | color: #fff; 40 | text-shadow:1px 1px 1px; 41 | border-top: 1px solid #312E3D; 42 | border-left: 1px solid #312E3D; 43 | border-right: 1px solid #312E3D; 44 | border-bottom: 1px solid #56536A; 45 | border-radius: 4px; 46 | background-color: #2D2D3F; 47 | } 48 | .but{ 49 | width: 300px; 50 | min-height: 20px; 51 | display: block; 52 | background-color: #4a77d4; 53 | border: 1px solid #3762bc; 54 | color: #fff; 55 | padding: 9px 14px; 56 | font-size: 15px; 57 | line-height: normal; 58 | border-radius: 5px; 59 | margin: 0; 60 | } -------------------------------------------------------------------------------- /src/main/index.css: -------------------------------------------------------------------------------- 1 | #main_container { 2 | background: #f4f4f4 no-repeat ; 3 | -moz-border-radius: 15px; 4 | border-radius: 15px; 5 | border: 5px solid #ebebeb; 6 | min-height: 300px; 7 | } 8 | body { 9 | font-family: Arial, Helvetica, sans-serif; 10 | color: #666; 11 | font-size: 14px; 12 | margin: 0; 13 | line-height: 18px; 14 | 15 | } 16 | #features { 17 | background: #c6db6f repeat-x top left; 18 | border-radius: 15px; 19 | border:1px solid #e0e8c6; 20 | margin:5px 0 10px 0; 21 | padding:20px 20px 20px 20px; 22 | } -------------------------------------------------------------------------------- /src/main/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
编号时间接收消息返回消息(值)
31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | const {ipcRenderer} = require('electron'); 2 | 3 | function switchCase() { 4 | var obj = document.getElementById('conType'); 5 | var index = obj.selectedIndex; //序号,取当前选中选项的序号 6 | var val = obj.options[index].value; 7 | 8 | var flag = ipcRenderer.sendSync('open-server', index); 9 | if (flag != null) { 10 | var btn = document.getElementById('sbtn'); 11 | var cbtn = document.getElementById('cbtn'); 12 | btn.disabled = true; 13 | cbtn.disabled = false; 14 | btn.innerHTML = "启动中..." 15 | } 16 | 17 | } 18 | function closeCase() { 19 | 20 | var temp = ipcRenderer.sendSync('close-server', "close"); 21 | if (temp != null) { 22 | var btn = document.getElementById('sbtn'); 23 | var cbtn = document.getElementById('cbtn'); 24 | btn.disabled = false; 25 | cbtn.disabled = true; 26 | btn.innerHTML = "启动" 27 | } 28 | 29 | } 30 | 31 | function relogin(){ 32 | 33 | var flag = ipcRenderer.sendSync('login-out', "out"); 34 | 35 | window.location.href="../login/index.html"; 36 | 37 | } 38 | 39 | function clearTable() { 40 | var tab = document.getElementById("myTable"); //获得表格 41 | tab.innerHTML = "" 42 | +"编号" 43 | +"时间" 44 | +"接收消息" 45 | +"返回消息(值)" 46 | +""; 47 | 48 | } 49 | 50 | ipcRenderer.on('print-msg',async(event, msg) => { 51 | 52 | var tab=document.getElementById("myTable"); //获得表格 53 | var colsNum = tab.rows.item(0).cells.length; //表格的列数 54 | //表格当前的行数 55 | var num = document.getElementById("myTable").rows.length; 56 | var rownum = num; //第N行增加 57 | tab.insertRow(rownum); 58 | for (var i = 0; i < 4; i++) { 59 | tab.rows[rownum].insertCell(i);//插入列 60 | if (i == 0) { 61 | tab.rows[rownum].cells[i].innerHTML =`${msg.id}`; 62 | } else if (i == 1) { 63 | tab.rows[rownum].cells[i].innerHTML = `${msg.date}`; 64 | } else if (i == 2) { 65 | tab.rows[rownum].cells[i].innerHTML = `${msg.msg}`; 66 | } else { 67 | tab.rows[rownum].cells[i].innerHTML = `${msg.value}`; 68 | } 69 | } 70 | 71 | 72 | }); -------------------------------------------------------------------------------- /user.js: -------------------------------------------------------------------------------- 1 | const requestUrl = "http://ezutai.senkins.top"; 2 | const userModel = { 3 | async login(phoneno,password){ 4 | let userInfo = {}; 5 | return userInfo; 6 | } 7 | } 8 | module.exports = userModel; -------------------------------------------------------------------------------- /user/user.js: -------------------------------------------------------------------------------- 1 | const messageModel = require("../service/common/messageModel") 2 | const fetch = require("node-fetch"); 3 | const crypto = require('crypto'); 4 | const url="101.132.157.198:3006" 5 | 6 | const user = { 7 | async sdklogin(phoneno, password) { 8 | const md5 = crypto.createHash("md5"); 9 | let newPwd = md5.update(password).digest("hex"); 10 | try { 11 | 12 | const userInfo = await this.syncGetTopic(url, phoneno, password); 13 | if (userInfo == false) { 14 | return false 15 | } 16 | //组装用户信息 17 | let ret = { 18 | phoneNo: userInfo.userName, 19 | userType: userInfo.userType, 20 | topic: userInfo.topic 21 | } 22 | return ret; 23 | } catch (e) { 24 | 25 | console.log(e); 26 | return false 27 | 28 | } 29 | 30 | }, 31 | async syncGetTopic(url,phoneNo,password){ 32 | const link = "http://"+url+"/user/login"; 33 | const res = await fetch(link, { 34 | body: JSON.stringify({phoneNo:phoneNo,password:password}), // must match 'Content-Type' header 35 | headers: { 'Content-Type': 'application/json'}, 36 | method: 'POST', // *GET, POST, PUT, DELETE, etc. 37 | }) 38 | 39 | const json = await res.json(); 40 | 41 | if(json.ret == 0){ 42 | 43 | return false; 44 | } 45 | return json 46 | } 47 | 48 | } 49 | module.exports = user --------------------------------------------------------------------------------