Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
I
iot_test
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
wubi
iot_test
Commits
57db46ca
Commit
57db46ca
authored
Sep 30, 2025
by
wubi
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
添加协议编辑器
parent
1f108d72
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
573 additions
and
0 deletions
+573
-0
Modbus-RTU.json
protocol_editor/Modbus-RTU.json
+313
-0
TCP.json
protocol_editor/TCP.json
+147
-0
index.html
protocol_editor/index.html
+20
-0
index.js
protocol_editor/index.js
+93
-0
No files found.
protocol_editor/Modbus-RTU.json
0 → 100644
View file @
57db46ca
{
"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];
\n
msg.payload = {{factor}};
\n
return 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
protocol_editor/TCP.json
0 → 100644
View file @
57db46ca
{
"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\n
var read_input_obj =
\"
{{read_input}}
\"\n
//read_input_obj =
\"
RS_SD
\"
;
\n
msg.payload = JSON.parse(msg.payload);
\n
const val2 = findJsonValue(msg.payload, read_input_obj);
\n
msg.payload = val2;
\n
if (read_input_obj ==
\"
{{read_input}}
\"
){
\n
node.error(
\"
读对象未设置!
\"
);
\n
}
\n
node.warn(msg.payload);
\n
return 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
protocol_editor/index.html
0 → 100644
View file @
57db46ca
<!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
protocol_editor/index.js
0 → 100644
View file @
57db46ca
// 加载 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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment