node-red-contrib-symi-modbus 2.7.4 → 2.7.6

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.
@@ -0,0 +1,286 @@
1
+ // Symi蓝牙Mesh网关协议处理模块
2
+ // 协议版本: V1.3.1
3
+ // 支持TCP/IP和串口通信
4
+
5
+ module.exports = function(RED) {
6
+ 'use strict';
7
+
8
+ // 协议常量
9
+ const PROTOCOL = {
10
+ HEADER: 0x53,
11
+ // 操作码
12
+ OP_GET_DEVICE_LIST: 0x12,
13
+ OP_DEVICE_CONTROL: 0x30,
14
+ OP_QUERY_STATUS: 0x32,
15
+ OP_SCENE_CONTROL: 0x34,
16
+ OP_STATUS_EVENT: 0x80,
17
+ OP_DEVICE_LIST_RESPONSE: 0x92,
18
+ OP_CONTROL_RESPONSE: 0xB0,
19
+ OP_SCENE_RESPONSE: 0xB4,
20
+ // 消息类型
21
+ MSG_TYPE_SWITCH: 0x02, // 开关状态(1-4路)
22
+ MSG_TYPE_DIMMER: 0x03, // 调光状态
23
+ MSG_TYPE_RGB: 0x04, // 五色调光
24
+ MSG_TYPE_CURTAIN: 0x05, // 窗帘动作
25
+ MSG_TYPE_CURTAIN_POS: 0x06, // 窗帘位置
26
+ MSG_TYPE_THERMOSTAT: 0x07, // 温控器
27
+ MSG_TYPE_SWITCH_6: 0x45, // 6路开关状态
28
+ // 设备类型
29
+ DEVICE_TYPE_SWITCH: 0x01,
30
+ DEVICE_TYPE_DIMMER: 0x02,
31
+ DEVICE_TYPE_DUAL_COLOR: 0x04,
32
+ DEVICE_TYPE_CURTAIN: 0x05,
33
+ DEVICE_TYPE_CARD_POWER: 0x09,
34
+ DEVICE_TYPE_THERMOSTAT: 0x0A,
35
+ DEVICE_TYPE_PIR: 0x0C,
36
+ DEVICE_TYPE_RGB: 0x18,
37
+ DEVICE_TYPE_THERMOSTAT_3IN1: 0x94
38
+ };
39
+
40
+ // 计算异或校验和
41
+ function calculateChecksum(buffer) {
42
+ let checksum = 0;
43
+ for (let i = 0; i < buffer.length; i++) {
44
+ checksum ^= buffer[i];
45
+ }
46
+ return checksum;
47
+ }
48
+
49
+ // 构建获取设备列表请求帧
50
+ function buildGetDeviceListFrame() {
51
+ const buffer = Buffer.from([
52
+ PROTOCOL.HEADER, // 0x53
53
+ PROTOCOL.OP_GET_DEVICE_LIST, // 0x12
54
+ 0x00 // Length = 0
55
+ ]);
56
+ const checksum = calculateChecksum(buffer);
57
+ return Buffer.concat([buffer, Buffer.from([checksum])]);
58
+ }
59
+
60
+ // 解析设备列表响应
61
+ function parseDeviceListResponse(frames) {
62
+ const devices = [];
63
+
64
+ for (let i = 0; i < frames.length; i++) {
65
+ const frame = frames[i];
66
+
67
+ // 验证帧头
68
+ if (frame[0] !== PROTOCOL.HEADER || frame[1] !== PROTOCOL.OP_DEVICE_LIST_RESPONSE) {
69
+ continue;
70
+ }
71
+
72
+ const sequence = frame[2];
73
+ const length = frame[3];
74
+
75
+ if (sequence === 0x00) {
76
+ // 第一帧:设备总数
77
+ const totalDevices = frame[4];
78
+ continue;
79
+ }
80
+
81
+ // 解析设备数据
82
+ let offset = 4;
83
+ while (offset + 10 <= frame.length - 1) {
84
+ const shortAddr = frame[offset] | (frame[offset + 1] << 8); // 小端序
85
+ const deviceType = frame[offset + 2];
86
+ const mac = Buffer.from([
87
+ frame[offset + 3],
88
+ frame[offset + 4],
89
+ frame[offset + 5],
90
+ frame[offset + 6],
91
+ frame[offset + 7],
92
+ frame[offset + 8]
93
+ ]);
94
+ const buttons = frame[offset + 9];
95
+
96
+ devices.push({
97
+ shortAddr: shortAddr,
98
+ type: deviceType,
99
+ mac: mac.toString('hex').toUpperCase().match(/.{2}/g).join(':'),
100
+ buttons: buttons
101
+ });
102
+
103
+ offset += 10;
104
+ }
105
+ }
106
+
107
+ return devices;
108
+ }
109
+
110
+ // 构建开关控制帧(带当前状态,保持其他路不变)
111
+ function buildSwitchControlFrame(shortAddr, buttonNumber, totalButtons, state, currentStates) {
112
+ let stateValue;
113
+
114
+ if (totalButtons === 1) {
115
+ // 单路开关
116
+ stateValue = state ? 0x02 : 0x01;
117
+ const buffer = Buffer.from([
118
+ PROTOCOL.HEADER,
119
+ PROTOCOL.OP_DEVICE_CONTROL,
120
+ 0x05, // Length
121
+ shortAddr & 0xFF,
122
+ (shortAddr >> 8) & 0xFF,
123
+ 0x00, // ACK = 0
124
+ 0x05, // 重传5次
125
+ PROTOCOL.MSG_TYPE_SWITCH,
126
+ stateValue
127
+ ]);
128
+ const checksum = calculateChecksum(buffer);
129
+ return Buffer.concat([buffer, Buffer.from([checksum])]);
130
+ } else if (totalButtons >= 2 && totalButtons <= 4) {
131
+ // 2-4路开关:使用当前状态构建完整状态值
132
+ if (currentStates && currentStates.length === totalButtons) {
133
+ stateValue = buildMultiSwitchState(currentStates, buttonNumber, state);
134
+ } else {
135
+ // 如果没有当前状态,只设置目标按钮,其他位设为00(保持不变)
136
+ const bitPos = (buttonNumber - 1) * 2;
137
+ stateValue = (state ? 0x02 : 0x01) << bitPos;
138
+ }
139
+
140
+ const buffer = Buffer.from([
141
+ PROTOCOL.HEADER,
142
+ PROTOCOL.OP_DEVICE_CONTROL,
143
+ 0x05, // Length
144
+ shortAddr & 0xFF,
145
+ (shortAddr >> 8) & 0xFF,
146
+ 0x00, // ACK = 0
147
+ 0x05, // 重传5次
148
+ PROTOCOL.MSG_TYPE_SWITCH,
149
+ stateValue
150
+ ]);
151
+ const checksum = calculateChecksum(buffer);
152
+ return Buffer.concat([buffer, Buffer.from([checksum])]);
153
+ } else if (totalButtons === 6) {
154
+ // 6路开关:2字节状态值(小端序)
155
+ let stateValue16;
156
+ if (currentStates && currentStates.length === totalButtons) {
157
+ stateValue16 = buildMultiSwitchState(currentStates, buttonNumber, state);
158
+ } else {
159
+ // 如果没有当前状态,只设置目标按钮,其他位设为00(保持不变)
160
+ const bitPos = (buttonNumber - 1) * 2;
161
+ stateValue16 = (state ? 0x02 : 0x01) << bitPos;
162
+ }
163
+
164
+ const buffer = Buffer.from([
165
+ PROTOCOL.HEADER,
166
+ PROTOCOL.OP_DEVICE_CONTROL,
167
+ 0x06, // Length
168
+ shortAddr & 0xFF,
169
+ (shortAddr >> 8) & 0xFF,
170
+ 0x00, // ACK = 0
171
+ 0x05, // 重传5次
172
+ PROTOCOL.MSG_TYPE_SWITCH,
173
+ stateValue16 & 0xFF,
174
+ (stateValue16 >> 8) & 0xFF
175
+ ]);
176
+ const checksum = calculateChecksum(buffer);
177
+ return Buffer.concat([buffer, Buffer.from([checksum])]);
178
+ }
179
+
180
+ return null;
181
+ }
182
+
183
+ // 解析状态事件帧
184
+ function parseStatusEvent(frame) {
185
+ // 验证帧头
186
+ if (frame[0] !== PROTOCOL.HEADER || frame[1] !== PROTOCOL.OP_STATUS_EVENT) {
187
+ return null;
188
+ }
189
+
190
+ const subOp = frame[2];
191
+ const length = frame[3];
192
+ const shortAddr = frame[4] | (frame[5] << 8); // 小端序
193
+ const msgType = frame[6];
194
+
195
+ const event = {
196
+ subOp: subOp,
197
+ shortAddr: shortAddr,
198
+ msgType: msgType
199
+ };
200
+
201
+ // 根据消息类型解析状态
202
+ if (msgType === PROTOCOL.MSG_TYPE_SWITCH) {
203
+ // 1-4路开关状态
204
+ const stateValue = frame[7];
205
+ event.states = parseMultiSwitchState(stateValue, 4);
206
+ } else if (msgType === PROTOCOL.MSG_TYPE_SWITCH_6) {
207
+ // 6路开关状态
208
+ const stateLow = frame[7];
209
+ const stateHigh = frame[8];
210
+ const stateValue = stateLow | (stateHigh << 8);
211
+ event.states = parseMultiSwitchState(stateValue, 6);
212
+ } else if (msgType === PROTOCOL.MSG_TYPE_DIMMER) {
213
+ // 调光灯状态
214
+ event.brightness = frame[7];
215
+ event.colorTemp = frame[8];
216
+ } else if (msgType === PROTOCOL.MSG_TYPE_CURTAIN) {
217
+ // 窗帘动作状态
218
+ event.action = frame[7]; // 0=停止, 1=打开中, 2=关闭中
219
+ } else if (msgType === PROTOCOL.MSG_TYPE_CURTAIN_POS) {
220
+ // 窗帘位置状态
221
+ event.position = frame[7]; // 0-100%
222
+ }
223
+
224
+ return event;
225
+ }
226
+
227
+ // 解析多路开关状态值
228
+ function parseMultiSwitchState(stateValue, totalButtons) {
229
+ const states = [];
230
+
231
+ for (let i = 0; i < totalButtons; i++) {
232
+ const bitPos = i * 2;
233
+ const bits = (stateValue >> bitPos) & 0x03;
234
+
235
+ if (bits === 0x01) {
236
+ states.push(false); // 关
237
+ } else if (bits === 0x02) {
238
+ states.push(true); // 开
239
+ } else {
240
+ states.push(null); // 保持不变或未知
241
+ }
242
+ }
243
+
244
+ return states;
245
+ }
246
+
247
+ // 构建多路开关状态值(用于控制时保持其他路不变)
248
+ function buildMultiSwitchState(currentStates, buttonNumber, newState) {
249
+ let stateValue = 0;
250
+
251
+ for (let i = 0; i < currentStates.length; i++) {
252
+ const bitPos = i * 2;
253
+ let bits;
254
+
255
+ if (i === buttonNumber - 1) {
256
+ // 要改变的按钮
257
+ bits = newState ? 0x02 : 0x01;
258
+ } else {
259
+ // 保持不变的按钮
260
+ if (currentStates[i] === true) {
261
+ bits = 0x02;
262
+ } else if (currentStates[i] === false) {
263
+ bits = 0x01;
264
+ } else {
265
+ bits = 0x00; // 保持不变
266
+ }
267
+ }
268
+
269
+ stateValue |= (bits << bitPos);
270
+ }
271
+
272
+ return stateValue;
273
+ }
274
+
275
+ return {
276
+ PROTOCOL,
277
+ calculateChecksum,
278
+ buildGetDeviceListFrame,
279
+ parseDeviceListResponse,
280
+ buildSwitchControlFrame,
281
+ parseStatusEvent,
282
+ parseMultiSwitchState,
283
+ buildMultiSwitchState
284
+ };
285
+ };
286
+
@@ -16,6 +16,7 @@
16
16
  var node = this;
17
17
  var stateCache = {}; // 缓存所有线圈状态
18
18
  var relayNamesCache = {}; // 缓存继电器名称
19
+ var pollInterval = null; // 轮询定时器(全局变量,用于清理)
19
20
 
20
21
  // 填充主站节点选择器
21
22
  var masterNodeSelect = $("#node-input-masterNode");
@@ -62,17 +63,27 @@
62
63
  return;
63
64
  }
64
65
 
65
- var masterNode = RED.nodes.node(masterNodeId);
66
- if (!masterNode || !masterNode.slaves || masterNode.slaves.length === 0) {
67
- container.html('<div style="padding: 40px; text-align: center; color: #999; font-size: 14px;">主站节点未配置从站</div>');
68
- return;
69
- }
66
+ // 显示加载中
67
+ container.html('<div style="padding: 40px; text-align: center; color: #999; font-size: 14px;"><i class="fa fa-spinner fa-spin"></i> 加载中...</div>');
68
+
69
+ // 通过HTTP API获取主站节点的最新配置
70
+ $.ajax({
71
+ url: '/modbus-dashboard/master-config/' + masterNodeId,
72
+ method: 'GET',
73
+ success: function(masterConfig) {
74
+ if (!masterConfig || !masterConfig.slaves || masterConfig.slaves.length === 0) {
75
+ container.html('<div style="padding: 40px; text-align: center; color: #999; font-size: 14px;">主站节点未配置从站</div>');
76
+ return;
77
+ }
78
+
79
+ // 加载继电器名称
80
+ loadRelayNames();
70
81
 
71
- // 加载继电器名称
72
- loadRelayNames();
82
+ // 清空容器
83
+ container.empty();
73
84
 
74
- // 遍历所有从站
75
- masterNode.slaves.forEach(function(slave) {
85
+ // 遍历所有从站
86
+ masterConfig.slaves.forEach(function(slave) {
76
87
  var slaveSection = $('<div class="slave-section">');
77
88
 
78
89
  var slaveHeader = $(`
@@ -113,20 +124,25 @@
113
124
  container.append(slaveSection);
114
125
  });
115
126
 
116
- // 绑定按钮点击事件
117
- $(".btn-toggle").off("click").on("click", function() {
118
- var slaveAddr = parseInt($(this).data("slave"));
119
- var coil = parseInt($(this).data("coil"));
120
- var key = slaveAddr + "_" + coil;
121
- var currentState = stateCache[key] || false;
122
- var newState = !currentState;
123
-
124
- // 发送控制命令(通过HTTP API)
125
- sendControlCommand(slaveAddr, coil, newState);
126
-
127
- // 立即更新UI(乐观更新)
128
- stateCache[key] = newState;
129
- updateButtonState($(this), newState);
127
+ // 绑定按钮点击事件
128
+ $(".btn-toggle").off("click").on("click", function() {
129
+ var slaveAddr = parseInt($(this).data("slave"));
130
+ var coil = parseInt($(this).data("coil"));
131
+ var key = slaveAddr + "_" + coil;
132
+ var currentState = stateCache[key] || false;
133
+ var newState = !currentState;
134
+
135
+ // 发送控制命令(通过HTTP API)
136
+ sendControlCommand(slaveAddr, coil, newState);
137
+
138
+ // 立即更新UI(乐观更新)
139
+ stateCache[key] = newState;
140
+ updateButtonState($(this), newState);
141
+ });
142
+ },
143
+ error: function(xhr, status, error) {
144
+ container.html('<div style="padding: 40px; text-align: center; color: #f44336; font-size: 14px;"><i class="fa fa-exclamation-triangle"></i> 加载失败: ' + error + '</div>');
145
+ }
130
146
  });
131
147
  }
132
148
 
@@ -164,7 +180,6 @@
164
180
  }
165
181
 
166
182
  // 轮询状态更新(每500ms)
167
- var pollInterval = null;
168
183
  function startPolling() {
169
184
  if (pollInterval) {
170
185
  clearInterval(pollInterval);
@@ -222,6 +237,15 @@
222
237
  }
223
238
  });
224
239
 
240
+ // 添加刷新按钮
241
+ $("#btn-refresh-dashboard").on("click", function() {
242
+ stopPolling();
243
+ renderDashboard();
244
+ if (masterNodeSelect.val()) {
245
+ startPolling();
246
+ }
247
+ });
248
+
225
249
  // 初始渲染
226
250
  renderDashboard();
227
251
  if (node.masterNode) {
@@ -232,6 +256,27 @@
232
256
  $("#node-dialog-cancel, #node-dialog-ok").on("click", function() {
233
257
  stopPolling();
234
258
  });
259
+ },
260
+ oneditcancel: function() {
261
+ // 取消编辑时停止轮询,防止内存泄漏
262
+ if (pollInterval) {
263
+ clearInterval(pollInterval);
264
+ pollInterval = null;
265
+ }
266
+ },
267
+ oneditsave: function() {
268
+ // 保存时停止轮询,防止内存泄漏
269
+ if (pollInterval) {
270
+ clearInterval(pollInterval);
271
+ pollInterval = null;
272
+ }
273
+ },
274
+ oneditdelete: function() {
275
+ // 删除节点时停止轮询,防止内存泄漏
276
+ if (pollInterval) {
277
+ clearInterval(pollInterval);
278
+ pollInterval = null;
279
+ }
235
280
  }
236
281
  });
237
282
  </script>
@@ -244,10 +289,13 @@
244
289
 
245
290
  <div class="form-row">
246
291
  <label for="node-input-masterNode"><i class="fa fa-microchip"></i> 主站节点</label>
247
- <select id="node-input-masterNode" style="width: 70%;">
292
+ <select id="node-input-masterNode" style="width: 55%;">
248
293
  <option value="">请选择主站节点</option>
249
294
  </select>
250
- <div style="font-size: 11px; color: #999; margin-top: 5px;">选择要监控的Modbus主站节点</div>
295
+ <button type="button" id="btn-refresh-dashboard" class="red-ui-button" style="margin-left: 5px;">
296
+ <i class="fa fa-refresh"></i> 刷新
297
+ </button>
298
+ <div style="font-size: 11px; color: #999; margin-top: 5px;">选择要监控的Modbus主站节点,点击刷新按钮更新显示</div>
251
299
  </div>
252
300
 
253
301
  <div class="form-row" style="margin-top: 20px;">
@@ -255,7 +303,7 @@
255
303
  <i class="fa fa-dashboard"></i> 控制面板
256
304
  </label>
257
305
  <div id="dashboard-container" style="
258
- max-height: 500px;
306
+ max-height: 700px;
259
307
  overflow-y: auto;
260
308
  border: 1px solid #ddd;
261
309
  border-radius: 4px;
@@ -62,6 +62,23 @@ module.exports = function(RED) {
62
62
 
63
63
  RED.nodes.registerType("modbus-dashboard", ModbusDashboardNode);
64
64
 
65
+ // HTTP API:获取主站节点配置
66
+ RED.httpAdmin.get('/modbus-dashboard/master-config/:id', function(req, res) {
67
+ var masterNodeId = req.params.id;
68
+ var masterNode = RED.nodes.getNode(masterNodeId);
69
+
70
+ if (!masterNode) {
71
+ res.status(404).json({error: '主站节点不存在'});
72
+ return;
73
+ }
74
+
75
+ // 返回主站配置(包括最新的从站列表)
76
+ res.json({
77
+ slaves: masterNode.slaves || [],
78
+ relayNames: masterNode.relayNames || {}
79
+ });
80
+ });
81
+
65
82
  // HTTP API:获取状态
66
83
  RED.httpAdmin.get('/modbus-dashboard/state', function(req, res) {
67
84
  res.json({
@@ -1170,6 +1170,10 @@ module.exports = function(RED) {
1170
1170
 
1171
1171
  node.isProcessingWrite = true;
1172
1172
 
1173
+ // 记录队列开始处理时间
1174
+ const queueStartTime = Date.now();
1175
+ const queueLength = node.writeQueue.length;
1176
+
1173
1177
  while (node.writeQueue.length > 0) {
1174
1178
  const task = node.writeQueue.shift();
1175
1179
 
@@ -1189,6 +1193,8 @@ module.exports = function(RED) {
1189
1193
  if (task.reject) {
1190
1194
  task.reject(err);
1191
1195
  }
1196
+ // 写入失败不中断队列,继续处理下一个任务
1197
+ node.warn(`队列任务失败,继续处理下一个任务: ${err.message}`);
1192
1198
  }
1193
1199
 
1194
1200
  // 等待一段时间再处理下一个任务(20ms间隔,确保总线稳定)
@@ -1197,6 +1203,12 @@ module.exports = function(RED) {
1197
1203
  }
1198
1204
  }
1199
1205
 
1206
+ // 队列处理完成,输出统计信息
1207
+ const queueDuration = Date.now() - queueStartTime;
1208
+ if (queueLength > 1) {
1209
+ node.debug(`写入队列处理完成:${queueLength}个任务,耗时${queueDuration}ms`);
1210
+ }
1211
+
1200
1212
  node.isProcessingWrite = false;
1201
1213
  };
1202
1214
 
@@ -1236,6 +1248,7 @@ module.exports = function(RED) {
1236
1248
  node.lastWriteTime[slaveId] = Date.now();
1237
1249
 
1238
1250
  // 更新本地状态
1251
+ const oldValue = node.deviceStates[slaveId].coils[coil];
1239
1252
  node.deviceStates[slaveId].coils[coil] = value;
1240
1253
 
1241
1254
  node.log(`写入成功: 从站${slaveId} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
@@ -1248,6 +1261,17 @@ module.exports = function(RED) {
1248
1261
  value: value
1249
1262
  });
1250
1263
 
1264
+ // 只在状态真正改变时广播状态变化事件(用于LED反馈)
1265
+ // 避免重复广播导致LED反馈死循环
1266
+ if (oldValue !== value) {
1267
+ RED.events.emit('modbus:coilStateChanged', {
1268
+ slave: slaveId,
1269
+ coil: coil,
1270
+ value: value,
1271
+ source: 'write'
1272
+ });
1273
+ }
1274
+
1251
1275
  // 释放锁
1252
1276
  node.modbusLock = false;
1253
1277
 
@@ -1325,14 +1349,29 @@ module.exports = function(RED) {
1325
1349
 
1326
1350
  // 更新本地状态
1327
1351
  for (let i = 0; i < values.length; i++) {
1328
- node.deviceStates[slaveId].coils[startCoil + i] = values[i];
1352
+ const coilIndex = startCoil + i;
1353
+ const oldValue = node.deviceStates[slaveId].coils[coilIndex];
1354
+ const newValue = values[i];
1355
+
1356
+ node.deviceStates[slaveId].coils[coilIndex] = newValue;
1357
+
1329
1358
  // 发布到MQTT和触发事件
1330
- node.publishMqttState(slaveId, startCoil + i, values[i]);
1359
+ node.publishMqttState(slaveId, coilIndex, newValue);
1331
1360
  node.emit('stateUpdate', {
1332
1361
  slave: slaveId,
1333
- coil: startCoil + i,
1334
- value: values[i]
1362
+ coil: coilIndex,
1363
+ value: newValue
1335
1364
  });
1365
+
1366
+ // 只在状态真正改变时广播状态变化事件(用于LED反馈)
1367
+ if (oldValue !== newValue) {
1368
+ RED.events.emit('modbus:coilStateChanged', {
1369
+ slave: slaveId,
1370
+ coil: coilIndex,
1371
+ value: newValue,
1372
+ source: 'write'
1373
+ });
1374
+ }
1336
1375
  }
1337
1376
 
1338
1377
  node.debug(`批量写入成功: 从站${slaveId} 起始线圈${startCoil} 共${values.length}个线圈`);
@@ -1396,18 +1435,10 @@ module.exports = function(RED) {
1396
1435
  node.log(`收到内部事件:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
1397
1436
 
1398
1437
  try {
1399
- // 执行写入操作
1438
+ // 执行写入操作(writeSingleCoil内部已经会广播状态变化事件)
1400
1439
  await node.writeSingleCoil(slave, coil, value);
1401
1440
 
1402
- // 写入成功后,广播状态变化事件(用于LED反馈)
1403
- RED.events.emit('modbus:coilStateChanged', {
1404
- slave: slave,
1405
- coil: coil,
1406
- value: value,
1407
- source: 'master'
1408
- });
1409
-
1410
- node.log(`内部事件写入成功,已广播状态变化:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
1441
+ node.log(`内部事件写入成功:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
1411
1442
  } catch (err) {
1412
1443
  node.error(`内部事件写入失败: 从站${slave} 线圈${coil} - ${err.message}`);
1413
1444
  }