node-red-contrib-symi-mesh 1.6.6 → 1.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,12 +2,12 @@
2
2
  {
3
3
  "id": "knx_sync_flow",
4
4
  "type": "tab",
5
- "label": "Symi Mesh KNX 双向同步",
5
+ "label": "Symi Mesh - KNX Sync",
6
6
  "disabled": false,
7
- "info": "展示Symi Mesh设备与KNX系统的完整双向同步,包括开关、调光灯、窗帘、温控器等所有设备类型"
7
+ "info": "Mesh设备与KNX系统双向同步示例"
8
8
  },
9
9
  {
10
- "id": "symi_gateway",
10
+ "id": "symi_gateway_knx",
11
11
  "type": "symi-gateway",
12
12
  "name": "Symi网关",
13
13
  "connectionType": "tcp",
@@ -17,449 +17,87 @@
17
17
  "baudRate": "115200"
18
18
  },
19
19
  {
20
- "id": "knx_gateway",
20
+ "id": "knx_gateway_config",
21
21
  "type": "knxUltimate-config",
22
22
  "host": "192.168.1.50",
23
23
  "port": "3671",
24
24
  "physAddr": "15.15.22",
25
25
  "hostProtocol": "TunnelUDP",
26
- "suppress_ack_ldatareq": false,
27
26
  "loglevel": "error",
28
- "name": "KNX网关",
29
- "localEchoInTunneling": true,
30
- "KNXEthInterface": "Auto",
31
- "KNXEthInterfaceManuallyInput": ""
27
+ "name": "KNX Gateway",
28
+ "delaybetweentelegrams": 25,
29
+ "autoReconnect": "yes"
32
30
  },
33
31
  {
34
- "id": "comment_switch_sync",
35
- "type": "comment",
36
- "z": "knx_sync_flow",
37
- "name": "开关双向同步 (Symi ↔ KNX)",
38
- "info": "Symi开关与KNX开关执行器双向同步",
39
- "x": 150,
40
- "y": 60,
41
- "wires": []
42
- },
43
- {
44
- "id": "symi_switch",
45
- "type": "symi-device",
46
- "z": "knx_sync_flow",
47
- "name": "三路开关",
48
- "gateway": "symi_gateway",
49
- "deviceMac": "14:27:c9:20:da:cc",
50
- "outputFormat": "full",
51
- "x": 130,
52
- "y": 120,
53
- "wires": [["switch_to_knx"]]
54
- },
55
- {
56
- "id": "switch_to_knx",
57
- "type": "function",
58
- "z": "knx_sync_flow",
59
- "name": "Symi→KNX转换",
60
- "func": "// Symi开关状态转换为KNX格式\nconst channel = msg.channel || 1;\nconst value = msg.payload ? 1 : 0;\n\n// 根据通道设置KNX组地址\nconst gaMap = {\n 1: '1/1/1',\n 2: '1/1/2',\n 3: '1/1/3'\n};\n\nreturn {\n payload: value,\n destination: gaMap[channel]\n};",
61
- "outputs": 1,
62
- "noerr": 0,
63
- "x": 330,
64
- "y": 120,
65
- "wires": [["knx_out_switch"]]
66
- },
67
- {
68
- "id": "knx_out_switch",
69
- "type": "knxUltimate-out",
32
+ "id": "knx_input_node",
33
+ "type": "knxUltimate",
70
34
  "z": "knx_sync_flow",
71
- "server": "knx_gateway",
72
- "name": "KNX开关输出",
35
+ "server": "knx_gateway_config",
73
36
  "topic": "",
74
37
  "dpt": "1.001",
75
- "initialread": false,
76
- "notifyreadrequest": false,
77
- "notifyresponse": false,
78
- "notifywrite": true,
79
- "notifyreadresponse": false,
80
- "notifyreadrequestalsorespondtobus": false,
81
- "notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized": "",
82
- "listenallga": false,
38
+ "name": "KNX Input",
83
39
  "outputtype": "write",
84
- "outputRBE": true,
85
- "inputRBE": true,
86
- "x": 560,
87
- "y": 120,
88
- "wires": []
89
- },
90
- {
91
- "id": "knx_in_switch",
92
- "type": "knxUltimate-in",
93
- "z": "knx_sync_flow",
94
- "server": "knx_gateway",
95
- "name": "KNX开关输入",
96
- "topic": "1/1/1",
97
- "dpt": "1.001",
98
- "initialread": false,
40
+ "outputRBE": "false",
41
+ "inputRBE": "false",
99
42
  "notifyreadrequest": false,
100
43
  "notifyresponse": true,
101
44
  "notifywrite": true,
102
- "notifyreadresponse": false,
103
- "notifyreadrequestalsorespondtobus": false,
104
- "notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized": "",
105
- "listenallga": false,
106
- "x": 140,
107
- "y": 180,
108
- "wires": [["knx_to_switch"]]
109
- },
110
- {
111
- "id": "knx_to_switch",
112
- "type": "function",
113
- "z": "knx_sync_flow",
114
- "name": "KNX→Symi转换",
115
- "func": "// KNX状态转换为Symi格式\n// 根据组地址确定通道\nconst gaChannelMap = {\n '1/1/1': 1,\n '1/1/2': 2,\n '1/1/3': 3\n};\n\nconst channel = gaChannelMap[msg.destination] || 1;\n\nreturn {\n payload: msg.payload === 1,\n channel: channel,\n deviceMac: '14:27:c9:20:da:cc'\n};",
116
- "outputs": 1,
117
- "noerr": 0,
118
- "x": 330,
119
- "y": 180,
120
- "wires": [["symi_switch_control"]]
121
- },
122
- {
123
- "id": "symi_switch_control",
124
- "type": "symi-device",
125
- "z": "knx_sync_flow",
126
- "name": "控制Symi开关",
127
- "gateway": "symi_gateway",
128
- "deviceMac": "",
129
- "outputFormat": "full",
130
- "x": 560,
131
- "y": 180,
132
- "wires": []
133
- },
134
- {
135
- "id": "comment_light_sync",
136
- "type": "comment",
137
- "z": "knx_sync_flow",
138
- "name": "调光灯双向同步 (Symi ↔ KNX)",
139
- "info": "Symi调光灯与KNX调光执行器双向同步",
140
- "x": 160,
141
- "y": 260,
142
- "wires": []
143
- },
144
- {
145
- "id": "symi_light",
146
- "type": "symi-device",
147
- "z": "knx_sync_flow",
148
- "name": "双色调光灯",
149
- "gateway": "symi_gateway",
150
- "deviceMac": "e4:53:4c:93:46:84",
151
- "outputFormat": "full",
152
- "x": 140,
153
- "y": 320,
154
- "wires": [["light_to_knx"]]
155
- },
156
- {
157
- "id": "light_to_knx",
158
- "type": "function",
159
- "z": "knx_sync_flow",
160
- "name": "Symi灯光→KNX",
161
- "func": "// Symi灯光状态转换为KNX DPT 1(开关) + DPT 5(亮度)\nconst msgs = [];\n\n// 开关状态\nif (msg.payload.switch !== undefined) {\n msgs.push({\n payload: msg.payload.switch ? 1 : 0,\n destination: '2/1/1', // KNX开关组地址\n dpt: '1.001'\n });\n}\n\n// 亮度\nif (msg.payload.brightness !== undefined) {\n msgs.push({\n payload: Math.round(msg.payload.brightness * 255 / 100),\n destination: '2/1/2', // KNX亮度组地址\n dpt: '5.001'\n });\n}\n\n// 色温(可选)\nif (msg.payload.colorTemp !== undefined) {\n msgs.push({\n payload: msg.payload.colorTemp,\n destination: '2/1/3', // KNX色温组地址\n dpt: '5.001'\n });\n}\n\nreturn [msgs];",
162
- "outputs": 1,
163
- "noerr": 0,
164
- "x": 340,
165
- "y": 320,
166
- "wires": [["knx_out_light"]]
167
- },
168
- {
169
- "id": "knx_out_light",
170
- "type": "knxUltimate-out",
171
- "z": "knx_sync_flow",
172
- "server": "knx_gateway",
173
- "name": "KNX调光输出",
174
- "topic": "",
175
- "dpt": "1.001",
176
- "initialread": false,
177
- "notifyreadrequest": false,
178
- "notifyresponse": false,
179
- "notifywrite": true,
180
- "notifyreadresponse": false,
181
- "notifyreadrequestalsorespondtobus": false,
182
- "notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized": "",
183
- "listenallga": false,
184
- "outputtype": "write",
185
- "outputRBE": true,
186
- "inputRBE": true,
187
- "x": 560,
188
- "y": 320,
189
- "wires": []
190
- },
191
- {
192
- "id": "knx_in_light_switch",
193
- "type": "knxUltimate-in",
194
- "z": "knx_sync_flow",
195
- "server": "knx_gateway",
196
- "name": "KNX灯光开关",
197
- "topic": "2/1/1",
198
- "dpt": "1.001",
199
- "initialread": false,
200
- "notifyreadrequest": false,
201
- "notifyresponse": true,
202
- "notifywrite": true,
203
- "notifyreadresponse": false,
204
- "notifyreadrequestalsorespondtobus": false,
205
- "notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized": "",
206
- "listenallga": false,
45
+ "listenallga": true,
207
46
  "x": 150,
208
- "y": 380,
209
- "wires": [["knx_light_to_symi"]]
47
+ "y": 100,
48
+ "wires": [["knx_bridge_node"]]
210
49
  },
211
50
  {
212
- "id": "knx_in_light_brightness",
213
- "type": "knxUltimate-in",
51
+ "id": "knx_bridge_node",
52
+ "type": "symi-knx-bridge",
214
53
  "z": "knx_sync_flow",
215
- "server": "knx_gateway",
216
- "name": "KNX亮度",
217
- "topic": "2/1/2",
218
- "dpt": "5.001",
219
- "initialread": false,
220
- "notifyreadrequest": false,
221
- "notifyresponse": true,
222
- "notifywrite": true,
223
- "notifyreadresponse": false,
224
- "notifyreadrequestalsorespondtobus": false,
225
- "notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized": "",
226
- "listenallga": false,
227
- "x": 130,
228
- "y": 420,
229
- "wires": [["knx_light_to_symi"]]
54
+ "name": "KNX Bridge",
55
+ "gateway": "symi_gateway_knx",
56
+ "mappings": "[]",
57
+ "knxEntities": "[]",
58
+ "x": 350,
59
+ "y": 100,
60
+ "wires": [["knx_output_node"], ["debug_node"]]
230
61
  },
231
62
  {
232
- "id": "knx_light_to_symi",
233
- "type": "function",
63
+ "id": "knx_output_node",
64
+ "type": "knxUltimate",
234
65
  "z": "knx_sync_flow",
235
- "name": "KNX→Symi灯光",
236
- "func": "// KNX状态转换为Symi MQTT格式\nconst mac = 'e4534c934684'; // 双色调光灯MAC(无冒号)\n\n// 根据DPT类型处理\nif (msg.dpt === '1.001') {\n // 开关控制\n return {\n topic: `symi_mesh/${mac}/light/set`,\n payload: JSON.stringify({\n state: msg.payload === 1 ? 'ON' : 'OFF'\n })\n };\n} else if (msg.dpt === '5.001') {\n // 亮度控制\n return {\n topic: `symi_mesh/${mac}/light/set`,\n payload: JSON.stringify({\n brightness: msg.payload // 0-255\n })\n };\n}\n\nreturn null;",
237
- "outputs": 1,
238
- "noerr": 0,
239
- "x": 340,
240
- "y": 400,
241
- "wires": [["mqtt_publish"]]
242
- },
243
- {
244
- "id": "comment_cover_sync",
245
- "type": "comment",
246
- "z": "knx_sync_flow",
247
- "name": "窗帘双向同步 (Symi ↔ KNX)",
248
- "info": "Symi窗帘与KNX窗帘执行器双向同步",
249
- "x": 150,
250
- "y": 480,
251
- "wires": []
252
- },
253
- {
254
- "id": "symi_cover",
255
- "type": "symi-device",
256
- "z": "knx_sync_flow",
257
- "name": "智能窗帘",
258
- "gateway": "symi_gateway",
259
- "deviceMac": "d4:86:c0:81:d8:14",
260
- "outputFormat": "full",
261
- "x": 130,
262
- "y": 540,
263
- "wires": [["cover_to_knx"]]
264
- },
265
- {
266
- "id": "cover_to_knx",
267
- "type": "function",
268
- "z": "knx_sync_flow",
269
- "name": "Symi窗帘→KNX",
270
- "func": "// Symi窗帘状态转换为KNX DPT 1(上下) + DPT 5(位置)\nconst msgs = [];\n\n// 运行状态转换\nif (msg.payload.curtainStatus !== undefined) {\n const status = msg.payload.curtainStatus;\n // 1=opening, 2=closing, 0/3=stopped\n if (status === 1) {\n msgs.push({\n payload: 1, // 上\n destination: '3/1/1',\n dpt: '1.008'\n });\n } else if (status === 2) {\n msgs.push({\n payload: 0, // 下\n destination: '3/1/1',\n dpt: '1.008'\n });\n }\n}\n\n// 位置\nif (msg.payload.curtainPosition !== undefined) {\n msgs.push({\n payload: Math.round(msg.payload.curtainPosition * 255 / 100),\n destination: '3/1/2', // KNX位置组地址\n dpt: '5.001'\n });\n}\n\nreturn [msgs];",
271
- "outputs": 1,
272
- "noerr": 0,
273
- "x": 330,
274
- "y": 540,
275
- "wires": [["knx_out_cover"]]
276
- },
277
- {
278
- "id": "knx_out_cover",
279
- "type": "knxUltimate-out",
280
- "z": "knx_sync_flow",
281
- "server": "knx_gateway",
282
- "name": "KNX窗帘输出",
66
+ "server": "knx_gateway_config",
283
67
  "topic": "",
284
- "dpt": "1.008",
285
- "initialread": false,
286
- "notifyreadrequest": false,
287
- "notifyresponse": false,
288
- "notifywrite": true,
289
- "notifyreadresponse": false,
290
- "notifyreadrequestalsorespondtobus": false,
291
- "notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized": "",
292
- "listenallga": false,
68
+ "dpt": "0",
69
+ "name": "KNX Output (Universal Mode)",
293
70
  "outputtype": "write",
294
- "outputRBE": true,
295
- "inputRBE": true,
296
- "x": 550,
297
- "y": 540,
298
- "wires": []
299
- },
300
- {
301
- "id": "knx_in_cover",
302
- "type": "knxUltimate-in",
303
- "z": "knx_sync_flow",
304
- "server": "knx_gateway",
305
- "name": "KNX窗帘控制",
306
- "topic": "3/1/1",
307
- "dpt": "1.008",
308
- "initialread": false,
309
- "notifyreadrequest": false,
310
- "notifyresponse": true,
311
- "notifywrite": true,
312
- "notifyreadresponse": false,
313
- "notifyreadrequestalsorespondtobus": false,
314
- "notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized": "",
315
- "listenallga": false,
316
- "x": 150,
317
- "y": 600,
318
- "wires": [["knx_cover_to_symi"]]
319
- },
320
- {
321
- "id": "knx_cover_to_symi",
322
- "type": "function",
323
- "z": "knx_sync_flow",
324
- "name": "KNX→Symi窗帘",
325
- "func": "// KNX窗帘控制转换为Symi MQTT格式\nconst mac = 'd486c081d814';\n\nif (msg.dpt === '1.008') {\n // 上下控制\n return {\n topic: `symi_mesh/${mac}/cover/set`,\n payload: msg.payload === 1 ? 'OPEN' : 'CLOSE'\n };\n} else if (msg.dpt === '5.001') {\n // 位置控制\n return {\n topic: `symi_mesh/${mac}/cover/position/set`,\n payload: Math.round(msg.payload * 100 / 255).toString()\n };\n}\n\nreturn null;",
326
- "outputs": 1,
327
- "noerr": 0,
328
- "x": 340,
329
- "y": 600,
330
- "wires": [["mqtt_publish"]]
331
- },
332
- {
333
- "id": "comment_climate_sync",
334
- "type": "comment",
335
- "z": "knx_sync_flow",
336
- "name": "温控器双向同步 (Symi ↔ KNX)",
337
- "info": "Symi温控器与KNX空调控制器双向同步",
338
- "x": 160,
339
- "y": 680,
340
- "wires": []
341
- },
342
- {
343
- "id": "symi_climate",
344
- "type": "symi-device",
345
- "z": "knx_sync_flow",
346
- "name": "温控器",
347
- "gateway": "symi_gateway",
348
- "deviceMac": "14:2e:68:46:8c:ac",
349
- "outputFormat": "full",
350
- "x": 120,
351
- "y": 740,
352
- "wires": [["climate_to_knx"]]
353
- },
354
- {
355
- "id": "climate_to_knx",
356
- "type": "function",
357
- "z": "knx_sync_flow",
358
- "name": "Symi温控→KNX",
359
- "func": "// Symi温控器状态转换为KNX DPT 9(温度) + DPT 20(模式)\nconst msgs = [];\n\n// 开关状态\nif (msg.payload.switch !== undefined) {\n msgs.push({\n payload: msg.payload.switch ? 1 : 0,\n destination: '4/1/1', // KNX空调开关\n dpt: '1.001'\n });\n}\n\n// 目标温度\nif (msg.payload.targetTemp !== undefined) {\n msgs.push({\n payload: msg.payload.targetTemp,\n destination: '4/1/2', // KNX温度设定\n dpt: '9.001'\n });\n}\n\n// 模式\nif (msg.payload.climateMode !== undefined) {\n // 1=cool, 2=heat, 3=fan_only, 4=dry\n msgs.push({\n payload: msg.payload.climateMode,\n destination: '4/1/3', // KNX模式\n dpt: '20.102'\n });\n}\n\n// 风速\nif (msg.payload.fanMode !== undefined) {\n msgs.push({\n payload: msg.payload.fanMode,\n destination: '4/1/4', // KNX风速\n dpt: '5.001'\n });\n}\n\nreturn [msgs];",
360
- "outputs": 1,
361
- "noerr": 0,
362
- "x": 320,
363
- "y": 740,
364
- "wires": [["knx_out_climate"]]
365
- },
366
- {
367
- "id": "knx_out_climate",
368
- "type": "knxUltimate-out",
369
- "z": "knx_sync_flow",
370
- "server": "knx_gateway",
371
- "name": "KNX温控输出",
372
- "topic": "",
373
- "dpt": "9.001",
374
- "initialread": false,
375
- "notifyreadrequest": false,
376
- "notifyresponse": false,
71
+ "outputRBE": "false",
72
+ "inputRBE": "false",
377
73
  "notifywrite": true,
378
- "notifyreadresponse": false,
379
- "notifyreadrequestalsorespondtobus": false,
380
- "notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized": "",
381
- "listenallga": false,
382
- "outputtype": "write",
383
- "outputRBE": true,
384
- "inputRBE": true,
385
- "x": 540,
386
- "y": 740,
74
+ "listenallga": true,
75
+ "x": 550,
76
+ "y": 100,
387
77
  "wires": []
388
78
  },
389
79
  {
390
- "id": "mqtt_publish",
391
- "type": "mqtt out",
80
+ "id": "debug_node",
81
+ "type": "debug",
392
82
  "z": "knx_sync_flow",
393
- "name": "MQTT发布",
394
- "topic": "",
395
- "qos": "0",
396
- "retain": "false",
397
- "respTopic": "",
398
- "contentType": "",
399
- "userProps": "",
400
- "correl": "",
401
- "expiry": "",
402
- "broker": "",
83
+ "name": "Sync Debug",
84
+ "active": true,
85
+ "tosidebar": true,
86
+ "console": false,
87
+ "tostatus": false,
88
+ "complete": "payload",
403
89
  "x": 550,
404
- "y": 600,
90
+ "y": 160,
405
91
  "wires": []
406
92
  },
407
93
  {
408
- "id": "comment_sensor_sync",
94
+ "id": "comment_usage",
409
95
  "type": "comment",
410
96
  "z": "knx_sync_flow",
411
- "name": "传感器同步 (Symi → KNX)",
412
- "info": "Symi传感器状态同步到KNX(单向)",
413
- "x": 150,
414
- "y": 820,
415
- "wires": []
416
- },
417
- {
418
- "id": "symi_motion",
419
- "type": "symi-device",
420
- "z": "knx_sync_flow",
421
- "name": "人体感应器",
422
- "gateway": "symi_gateway",
423
- "deviceMac": "f3:cf:7d:3d:44:9c",
424
- "outputFormat": "full",
97
+ "name": "使用说明",
98
+ "info": "1. 修改symi_gateway_knx的IP为网关实际地址\n2. 修改knx_gateway_config的IP为KNX网关地址\n3. 双击KNX Bridge节点配置映射:\n - 导入KNX实体(组地址)\n - 添加Mesh设备与KNX实体的映射\n4. 部署后等待20秒初始化完成\n5. 测试双向同步",
425
99
  "x": 140,
426
- "y": 880,
427
- "wires": [["motion_to_knx"]]
428
- },
429
- {
430
- "id": "motion_to_knx",
431
- "type": "function",
432
- "z": "knx_sync_flow",
433
- "name": "人体感应→KNX",
434
- "func": "// Symi人体感应转换为KNX DPT 1\nif (msg.payload.motion !== undefined) {\n return {\n payload: msg.payload.motion ? 1 : 0,\n destination: '5/1/1', // KNX人体感应组地址\n dpt: '1.002'\n };\n}\nreturn null;",
435
- "outputs": 1,
436
- "noerr": 0,
437
- "x": 330,
438
- "y": 880,
439
- "wires": [["knx_out_motion"]]
440
- },
441
- {
442
- "id": "knx_out_motion",
443
- "type": "knxUltimate-out",
444
- "z": "knx_sync_flow",
445
- "server": "knx_gateway",
446
- "name": "KNX人体感应",
447
- "topic": "",
448
- "dpt": "1.002",
449
- "initialread": false,
450
- "notifyreadrequest": false,
451
- "notifyresponse": false,
452
- "notifywrite": true,
453
- "notifyreadresponse": false,
454
- "notifyreadrequestalsorespondtobus": false,
455
- "notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized": "",
456
- "listenallga": false,
457
- "outputtype": "write",
458
- "outputRBE": true,
459
- "inputRBE": true,
460
- "x": 540,
461
- "y": 880,
100
+ "y": 40,
462
101
  "wires": []
463
102
  }
464
103
  ]
465
-
package/lib/tcp-client.js CHANGED
@@ -57,9 +57,13 @@ class TCPClient extends EventEmitter {
57
57
  }
58
58
  }, 10000);
59
59
 
60
- // 捕获connect过程中的同步错误(如AggregateError
60
+ // 使用family:4强制IPv4,避免Node.js 18+ Happy Eyeballs导致AggregateError
61
61
  try {
62
- this.client.connect(this.port, this.host, () => {
62
+ this.client.connect({
63
+ port: this.port,
64
+ host: this.host,
65
+ family: 4 // 强制IPv4
66
+ }, () => {
63
67
  if (!resolved && !rejected) {
64
68
  resolved = true;
65
69
  clearTimeout(timeout);
@@ -6,6 +6,37 @@
6
6
  const { SerialPort } = require('serialport');
7
7
  const net = require('net');
8
8
 
9
+ // 全局禁用 Happy Eyeballs 算法,防止 AggregateError 导致 Node-RED 崩溃
10
+ // 这会影响所有使用 net.Socket 的模块(包括第三方模块如 KNX Ultimate)
11
+ if (typeof net.setDefaultAutoSelectFamily === 'function') {
12
+ net.setDefaultAutoSelectFamily(false);
13
+ }
14
+
15
+ // 全局未捕获异常处理 - 防止网络错误导致 Node-RED 崩溃
16
+ if (!global._symiErrorHandlerInstalled) {
17
+ global._symiErrorHandlerInstalled = true;
18
+
19
+ process.on('uncaughtException', (err) => {
20
+ // 网络相关错误不崩溃
21
+ const netErrors = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENETUNREACH', 'EHOSTUNREACH', 'ENOTFOUND'];
22
+ if (err && err.code && netErrors.includes(err.code)) {
23
+ console.error('[symi] 网络错误已捕获,继续运行:', err.message);
24
+ return; // 不崩溃
25
+ }
26
+ // AggregateError
27
+ if (err && (err.name === 'AggregateError' || (err.errors && Array.isArray(err.errors)))) {
28
+ console.error('[symi] AggregateError已捕获:', err.message);
29
+ return; // 不崩溃
30
+ }
31
+ // 其他错误打印但不崩溃(保护Node-RED)
32
+ console.error('[symi] 未捕获异常:', err);
33
+ });
34
+
35
+ process.on('unhandledRejection', (reason) => {
36
+ console.error('[symi] Promise rejection:', reason?.message || reason);
37
+ });
38
+ }
39
+
9
40
  module.exports = function(RED) {
10
41
 
11
42
  function SymiRS485ConfigNode(config) {
@@ -114,7 +145,7 @@ module.exports = function(RED) {
114
145
 
115
146
  node.client.on('connect', () => {
116
147
  node.connected = true;
117
- node.warn(`[RS485] TCP已连接: ${node.host}:${node.port}`);
148
+ node.log(`[RS485] TCP已连接: ${node.host}:${node.port}`);
118
149
  node.emit('connected');
119
150
  });
120
151
 
@@ -151,9 +182,13 @@ module.exports = function(RED) {
151
182
  }
152
183
  });
153
184
 
154
- // 使用try-catch包装connect,防止AggregateError导致崩溃
185
+ // 使用family:4强制IPv4,避免Node.js 18+ Happy Eyeballs导致AggregateError
155
186
  try {
156
- node.client.connect(node.port, node.host);
187
+ node.client.connect({
188
+ port: node.port,
189
+ host: node.host,
190
+ family: 4 // 强制IPv4,避免IPv6连接失败导致AggregateError
191
+ });
157
192
  } catch (connectErr) {
158
193
  node.error(`RS485 TCP连接异常: ${connectErr.message}`);
159
194
  }
@@ -2,36 +2,36 @@
2
2
  * Symi Gateway Configuration Node
3
3
  */
4
4
 
5
+ const net = require('net');
5
6
  const TCPClient = require('../lib/tcp-client');
6
7
  const SerialClient = require('../lib/serial-client');
7
8
  const { DeviceManager } = require('../lib/device-manager');
8
9
  const { ProtocolHandler, parseStatusEvent, OP_RESP_DEVICE_LIST } = require('../lib/protocol');
9
10
 
10
- // 全局异常处理 - 防止AggregateError等网络错误导致Node-RED崩溃
11
- // 只在模块首次加载时设置一次
12
- if (!global._symiGatewayErrorHandlerSet) {
13
- global._symiGatewayErrorHandlerSet = true;
11
+ // 全局禁用 Happy Eyeballs 算法
12
+ if (typeof net.setDefaultAutoSelectFamily === 'function') {
13
+ net.setDefaultAutoSelectFamily(false);
14
+ }
15
+
16
+ // 全局未捕获异常处理 - 防止网络错误导致 Node-RED 崩溃
17
+ if (!global._symiErrorHandlerInstalled) {
18
+ global._symiErrorHandlerInstalled = true;
14
19
 
15
20
  process.on('uncaughtException', (err) => {
16
- // 只处理网络相关的错误,其他错误继续抛出
17
- if (err.name === 'AggregateError' ||
18
- (err.code && ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENETUNREACH', 'EHOSTUNREACH'].includes(err.code))) {
19
- console.error('[symi-gateway] 网络异常已捕获,避免崩溃:', err.message || err);
20
- // 不重新抛出,防止崩溃
21
- } else {
22
- // 非网络错误,继续原有行为
23
- throw err;
21
+ const netErrors = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENETUNREACH', 'EHOSTUNREACH', 'ENOTFOUND', 'EADDRNOTAVAIL'];
22
+ if (err && err.code && netErrors.includes(err.code)) {
23
+ console.error('[symi] 网络错误已捕获,继续运行:', err.message);
24
+ return;
24
25
  }
26
+ if (err && (err.name === 'AggregateError' || (err.errors && Array.isArray(err.errors)))) {
27
+ console.error('[symi] AggregateError已捕获:', err.message);
28
+ return;
29
+ }
30
+ console.error('[symi] 未捕获异常:', err);
25
31
  });
26
32
 
27
- process.on('unhandledRejection', (reason, promise) => {
28
- // 检查是否是网络相关的Promise rejection
29
- if (reason && (reason.name === 'AggregateError' ||
30
- (reason.code && ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENETUNREACH', 'EHOSTUNREACH'].includes(reason.code)))) {
31
- console.error('[symi-gateway] 网络Promise rejection已捕获:', reason.message || reason);
32
- // 不重新抛出,防止崩溃
33
- }
34
- // 其他rejection由Node-RED默认处理
33
+ process.on('unhandledRejection', (reason) => {
34
+ console.error('[symi] Promise rejection:', reason?.message || reason);
35
35
  });
36
36
  }
37
37