Commit 57db46ca authored by wubi's avatar wubi

添加协议编辑器

parent 1f108d72
{
"name": "Modbus-RTU",
"config": [
{
"name": "从机地址",
"key": "unitid",
"type": "number",
"value": null,
"rules": [
{
"required": true,
"message": "从机地址不能为空",
"trigger": "blur"
}
],
"min": 1,
"max": 247,
"width": "500px",
"hint": "Modbus从机设备地址(1-247)"
},
{
"name": "功能码",
"key": "dataType",
"type": "select",
"value": null,
"option": [
{
"label": "功能码1:读取线圈状态",
"value": "Coil"
},
{
"label": "功能码2:读取输入状态",
"value": "Input"
},
{
"label": "功能码3:读取保持寄存器",
"value": "HoldingRegister"
},
{
"label": "功能码4:读取输入寄存器",
"value": "InputRegister"
}
],
"rules": [
{
"required": true,
"message": "功能码不能为空",
"trigger": "change"
}
],
"hint": "选择Modbus功能码类型"
},
{
"name": "地址",
"key": "adr",
"type": "number",
"value": null,
"rules": [
{
"required": true,
"message": "寄存器地址不能为空",
"trigger": "blur"
}
],
"min": 0,
"max": 65535,
"width": "500px",
"hint": "寄存器起始地址(十进制)"
},
{
"name": "数量",
"key": "quantity",
"type": "number",
"value": null,
"rules": [
{
"required": true,
"message": "读取数量不能为空",
"trigger": "blur"
}
],
"min": 1,
"max": 200,
"width": "500px",
"hint": "需要读取的寄存器数量"
},
{
"name": "数据转换",
"key": "factor",
"type": "string",
"value": "x",
"rules": [
{
"required": true,
"message": "数据转换不能为空",
"trigger": "blur"
}
],
"width": "500px",
"hint": "用于转换采集到的数据,如 x*0.1,(x-2000)/100",
"maxlength": 200
}
],
"nodes": [
{
"id": "da7a0f42ae981076",
"type": "subflow",
"name": "被测设备",
"info": "",
"category": "",
"in": [
{
"x": 60,
"y": 140,
"wires": [
{
"id": "7d08042446ad6297"
}
]
}
],
"out": [
{
"x": 580,
"y": 140,
"wires": [
{
"id": "4f986ed7787cc630",
"port": 0
}
]
}
],
"env": [],
"meta": {},
"color": "#DDAA99"
},
{
"id": "7d08042446ad6297",
"type": "modbus-getter",
"z": "da7a0f42ae981076",
"name": "",
"showStatusActivities": true,
"showErrors": true,
"showWarnings": true,
"logIOActivities": false,
"unitid": "1",
"dataType": "HoldingRegister",
"adr": "1",
"quantity": "2",
"server": "4cd290346f79a501",
"useIOFile": false,
"ioFile": "",
"useIOForPayload": false,
"emptyMsgOnFail": true,
"keepMsgProperties": true,
"delayOnStart": false,
"startDelayTime": "",
"x": 220,
"y": 140,
"wires": [
[
"4f986ed7787cc630"
],
[]
]
},
{
"id": "4f986ed7787cc630",
"type": "function",
"z": "da7a0f42ae981076",
"name": "数据转换",
"func": "var x = msg.payload[0];\nmsg.payload = {{factor}};\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 420,
"y": 140,
"wires": [
[]
]
},
{
"id": "d2ceed10c706fc5a",
"type": "comment",
"z": "da7a0f42ae981076",
"name": "3.数据采集",
"info": "",
"x": 200,
"y": 100,
"wires": []
},
{
"id": "4cd290346f79a501",
"type": "modbus-client",
"name": "",
"clienttype": "serial",
"bufferCommands": true,
"stateLogEnabled": false,
"queueLogEnabled": false,
"failureLogEnabled": true,
"tcpHost": "127.0.0.1",
"tcpPort": "502",
"tcpType": "DEFAULT",
"serialPort": "/dev/ttyS4",
"serialType": "RTU-BUFFERD",
"serialBaudrate": "9600",
"serialDatabits": "8",
"serialStopbits": "1",
"serialParity": "none",
"serialConnectionDelay": "100",
"serialAsciiResponseStartDelimiter": "0x3A",
"unit_id": "1",
"commandDelay": "1",
"clientTimeout": "1000",
"reconnectOnTimeout": true,
"reconnectTimeout": "2000",
"parallelUnitIdsAllowed": true,
"showErrors": false,
"showWarnings": true,
"showLogs": true
}
],
"map": {
"interface": [
{
"key": "ck",
"replace": {
"id": "4cd290346f79a501",
"name": "serialPort",
"type": "string"
}
},
{
"key": "bt",
"replace": {
"id": "4cd290346f79a501",
"name": "serialBaudrate",
"type": "string"
}
},
{
"key": "sj",
"replace": {
"id": "4cd290346f79a501",
"name": "serialDatabits",
"type": "string"
}
},
{
"key": "tz",
"replace": {
"id": "4cd290346f79a501",
"name": "serialStopbits",
"type": "string"
}
},
{
"key": "jy",
"replace": {
"id": "4cd290346f79a501",
"name": "serialParity",
"type": "string"
}
}
],
"protocol": [
{
"key": "unitid",
"replace": {
"id": "7d08042446ad6297",
"name": "unitid",
"type": "string"
}
},
{
"key": "dataType",
"replace": {
"id": "7d08042446ad6297",
"name": "dataType",
"type": "string"
}
},
{
"key": "adr",
"replace": {
"id": "7d08042446ad6297",
"name": "adr",
"type": "string"
}
},
{
"key": "quantity",
"replace": {
"id": "7d08042446ad6297",
"name": "quantity",
"type": "string"
}
},
{
"key": "factor",
"replace": {
"id": "4f986ed7787cc630",
"name": "{{factor}}",
"type": "content"
}
}
]
}
}
\ No newline at end of file
{
"name": "TCP",
"config": [
{
"name": "分隔符号",
"key": "newline",
"type": "string",
"value": "\\n",
"rules": [
{
"required": true,
"message": "分隔符号不能为空",
"trigger": "blur"
}
],
"width": "500px",
"hint": "字符串消息的分隔符号,如\\n",
"maxlength": 200
},
{
"name": "读对象",
"key": "read_obj",
"type": "string",
"value": null,
"rules": [
{
"required": true,
"message": "读对象不能为空",
"trigger": "blur"
}
],
"width": "500px",
"hint": "读取接收的消息中的指定对象的数据,如读{obj:100}中obj的值,填:obj;如读取{list:[1,2,3,4]}中的第1个元素,填:list[0",
"maxlength": 200
}
],
"nodes": [
{
"id": "da7a0f42ae981076",
"type": "subflow",
"name": "被测设备",
"info": "",
"category": "",
"in": [
{
"x": 60,
"y": 140,
"wires": []
}
],
"out": [
{
"x": 580,
"y": 140,
"wires": [
{
"id": "d80b7fa87a21f707",
"port": 0
}
]
}
],
"env": [],
"meta": {},
"color": "#DDAA99"
},
{
"id": "65b6cccf9c9b2c4f",
"type": "tcp in",
"z": "da7a0f42ae981076",
"name": "",
"server": "server",
"host": "",
"port": "8011",
"datamode": "stream",
"datatype": "utf8",
"newline": "\\n",
"topic": "",
"trim": false,
"base64": false,
"tls": "",
"x": 220,
"y": 260,
"wires": [
[
"d80b7fa87a21f707"
]
]
},
{
"id": "d80b7fa87a21f707",
"type": "function",
"z": "da7a0f42ae981076",
"name": "读对象",
"func": "function findJsonValue(jsonObj, path) {\n node.error(jsonObj);\n // 辅助函数:通过完整路径遍历获取值\n const traverse = (current, parts) => {\n for (let i = 0; i < parts.length; i++) {\n if (typeof current !== 'object' || current === null) {\n return undefined;\n }\n const part = parts[i];\n const isArrayIndex = /^\\d+$/.test(part); // 检查是否为纯数字(数组索引)\n\n if (isArrayIndex && Array.isArray(current)) {\n const index = parseInt(part, 10);\n if (index < current.length) {\n current = current[index];\n } else {\n return undefined; // 索引越界\n }\n } else if (typeof current === 'object' && current !== null && Object.prototype.hasOwnProperty.call(current, part)) {\n current = current[part];\n } else {\n return undefined; // 路径部分不存在\n }\n\n // 如果当前值是字符串,并且不是路径的最后一部分,尝试解析它看是否为JSON字符串\n if (typeof current === 'string' && i < parts.length - 1) {\n try {\n const parsedJson = JSON.parse(current);\n if (typeof parsedJson === 'object' && parsedJson !== null) {\n current = parsedJson; // 如果是合法的JSON对象/数组,则用解析后的结果继续遍历\n }\n } catch (e) {\n // 不是合法的JSON字符串,或者解析失败,则保持 current 不变,下一轮循环可能会因此返回 undefined\n }\n }\n }\n return current;\n };\n\n // 辅助函数:深度优先搜索对象中的键\n // 返回找到的第一个匹配键的值\n const deepSearch = (obj, keyToFind) => {\n if (typeof obj !== 'object' || obj === null) {\n return undefined;\n }\n\n // 直接在当前对象层级查找\n if (Object.prototype.hasOwnProperty.call(obj, keyToFind)) {\n return obj[keyToFind];\n }\n\n // 遍历对象的每个属性/元素\n for (const k in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, k)) {\n const item = obj[k];\n let foundValue;\n\n // 如果属性值是对象或数组,递归搜索\n if (typeof item === 'object' && item !== null) {\n foundValue = deepSearch(item, keyToFind);\n }\n // 如果属性值是字符串,尝试解析为JSON并搜索\n else if (typeof item === 'string') {\n try {\n const parsedJson = JSON.parse(item);\n if (typeof parsedJson === 'object' && parsedJson !== null) {\n // 先检查解析后的JSON顶层是否有该键,然后才考虑深度搜索解析后的内容\n if (Object.prototype.hasOwnProperty.call(parsedJson, keyToFind)) {\n foundValue = parsedJson[keyToFind];\n } else {\n foundValue = deepSearch(parsedJson, keyToFind);\n }\n }\n } catch (e) {\n // 不是可解析的JSON字符串\n }\n }\n\n if (foundValue !== undefined) {\n return foundValue; // 一旦找到,立即返回\n }\n }\n }\n return undefined; // 在当前对象及其子结构中未找到\n };\n\n // 主函数逻辑开始\n if (typeof jsonObj !== 'object' || jsonObj === null || typeof path !== 'string') {\n return undefined;\n }\n\n \n\n // 判断路径是简单键名还是复杂路径\n if (path.includes('.') || path.includes('[')) {\n // 复杂路径:使用路径遍历逻辑\n const parts = path.split(/[.\\[\\]]+/).filter(Boolean); // 分割路径并移除空字符串\n return traverse(jsonObj, parts);\n } else {\n // 简单键名:\n // 1. 首先尝试直接从顶层获取 (更符合直接输入键名的直觉)\n if (Object.prototype.hasOwnProperty.call(jsonObj, path)) {\n return jsonObj[path];\n }\n // 2. 如果顶层没有,则进行深度搜索\n return deepSearch(jsonObj, path);\n }\n}\n\nvar read_input_obj = \"{{read_input}}\"\n//read_input_obj = \"RS_SD\";\nmsg.payload = JSON.parse(msg.payload);\nconst val2 = findJsonValue(msg.payload, read_input_obj);\nmsg.payload = val2;\nif (read_input_obj == \"{{read_input}}\"){\n node.error(\"读对象未设置!\");\n}\nnode.warn(msg.payload);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 390,
"y": 260,
"wires": [
[]
]
}
],
"map": {
"interface": [
{
"key": "fw",
"replace": {
"id": "65b6cccf9c9b2c4f",
"name": "host",
"type": "string"
}
},
{
"key": "dk",
"replace": {
"id": "65b6cccf9c9b2c4f",
"name": "port",
"type": "string"
}
}
],
"protocol": [
{
"key": "newline",
"replace": {
"id": "65b6cccf9c9b2c4f",
"name": "newline",
"type": "string"
}
},
{
"key": "read_obj",
"replace": {
"id": "d80b7fa87a21f707",
"name": "{{read_input}}",
"type": "content"
}
}
]
}
}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Visual Editor</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body class="p-4">
<div id="tabs" class="flex space-x-4 mb-4">
<button data-tab="config" class="tab-btn">UI 配置</button>
<button data-tab="nodes" class="tab-btn">Node-RED 节点</button>
<button data-tab="map" class="tab-btn">替换规则</button>
</div>
<div id="content"></div>
<button id="exportBtn" class="mt-4 px-4 py-2 border rounded">导出 JSON</button>
<script src="index.js"></script>
</body>
</html>
\ No newline at end of file
// 加载 JSON
const modbus = JSON.parse(window.api.loadJSON('Modbus-RTU.json'));
const tcp = JSON.parse(window.api.loadJSON('TCP.json'));
const data = { ...modbus, ...tcp };
const tabs = document.querySelectorAll('.tab-btn');
const content = document.getElementById('content');
const exportBtn= document.getElementById('exportBtn');
let currentTab = 'config';
// 渲染函数
function render() {
content.innerHTML = '';
if (currentTab === 'config') renderConfig();
if (currentTab === 'nodes') renderNodes();
if (currentTab === 'map') renderMap();
}
// UI 配置
function renderConfig() {
data.config.forEach((item, i) => {
const wrapper = document.createElement('div');
wrapper.className = 'mb-2';
const label = document.createElement('label');
label.textContent = item.name + ':';
wrapper.appendChild(label);
let input;
if (item.type === 'number' || item.type === 'string') {
input = document.createElement('input');
input.type = item.type;
input.value = item.value || '';
}
if (item.type === 'select') {
input = document.createElement('select');
item.option.forEach(opt => {
const o = document.createElement('option');
o.value = opt.value;
o.textContent = opt.label;
if (opt.value === item.value) o.selected = true;
input.appendChild(o);
});
}
input.onchange = e => (data.config[i].value = e.target.value);
wrapper.appendChild(input);
content.appendChild(wrapper);
});
}
// Node-RED 节点
function renderNodes() {
data.nodes.forEach(node => {
const card = document.createElement('div');
card.className = 'border p-2 mb-2 rounded';
card.innerHTML = `<strong>${node.name || node.type}</strong><br/>ID: ${node.id}<br/>类型: ${node.type}`;
content.appendChild(card);
});
}
// 替换规则
function renderMap() {
['interface', 'protocol'].forEach(section => {
data.map[section].forEach(m => {
const card = document.createElement('div');
card.className = 'border p-2 mb-2 rounded';
card.innerHTML = `<strong>${section} - ${m.key}</strong><br/>节点ID: ${m.replace.id}<br/>属性: ${m.replace.name}`;
content.appendChild(card);
});
});
}
// 切换 Tabs
tabs.forEach(btn => btn.onclick = () => {
currentTab = btn.dataset.tab;
tabs.forEach(b => b.classList.remove('font-bold'));
btn.classList.add('font-bold');
render();
});
// 导出 JSON
exportBtn.onclick = () => {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'export.json';
a.click();
URL.revokeObjectURL(url);
};
// 初始渲染
tabs[0].classList.add('font-bold');
render();
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment