node-zserial 1.0.34 → 1.0.37

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/zserial.js +100 -95
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-zserial",
3
- "version": "1.0.34",
3
+ "version": "1.0.37",
4
4
  "description": "Node-RED nodes to talk to serial ports",
5
5
  "dependencies": {
6
6
  "serialport": "^12.0.0"
package/zserial.js CHANGED
@@ -1,8 +1,4 @@
1
1
 
2
- /**
3
- *
4
- * version: 1.0.34
5
- */
6
2
  module.exports = function (RED) {
7
3
  /*jshint -W082 */
8
4
  "use strict";
@@ -373,7 +369,7 @@ module.exports = function (RED) {
373
369
  for (var i = 0; i < msg.serialConfigs.length; i++) {
374
370
  var serialConfig = msg.serialConfigs[i];
375
371
  serialConfig._msgid = msg._msgid + "_" + i;
376
- getSerialServer(msg, serialConfig, done);
372
+ getSerialServer( msg, serialConfig, done);
377
373
  }
378
374
  }
379
375
 
@@ -556,12 +552,12 @@ module.exports = function (RED) {
556
552
  })
557
553
  }
558
554
 
559
- function afterClosed(port) {
555
+ function afterClosed(port){
560
556
  node[`_dataHandler_${port}`] = null
561
557
  node[`_timeoutHandler_${port}`] = null
562
558
  }
563
559
 
564
- if (!node._afterClosed) {
560
+ if(!node._afterClosed){
565
561
  node._afterClosed = afterClosed;
566
562
  serialPool.on('afterClosed', node._afterClosed);
567
563
  }
@@ -649,6 +645,7 @@ module.exports = function (RED) {
649
645
  addchar = addchar.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\e", "\e").replace("\\f", "\f").replace("\\0", "\0"); // jshint ignore:line
650
646
 
651
647
  if (addchar.substr(0, 2) == "0x") { addchar = new Buffer.from([addchar]); }
648
+ let assembleBuf = Buffer.alloc(0);
652
649
  connections[id] = (function () {
653
650
  var obj = {
654
651
  _emitter: new events.EventEmitter(),
@@ -659,11 +656,12 @@ module.exports = function (RED) {
659
656
  _retryNum: 0,
660
657
  tout: null,
661
658
  queue: [],
662
- // ---- 防重复推进:同一次 serial 'data' 回调只允许 dequeue 一次 ----
659
+ // 防重复推进:同一次 serial 'data' 回调只允许 dequeue 一次。
663
660
  _rxToken: 0,
664
661
  _rxDequeuedToken: -1,
665
- // ---- 发送序号:用于标记本次 writehead 的队首请求 ----
662
+ // 发送序号:用于标记本次 writehead 的队首请求。
666
663
  _txSeq: 0,
664
+ _framePending: null,
667
665
  on: function (a, b) { this._emitter.on(a, b); },
668
666
  once: function (a, b) { this._emitter.once(a, b); },
669
667
  close: function (cb) {
@@ -709,24 +707,36 @@ module.exports = function (RED) {
709
707
  writehead: function () {
710
708
  if (!this.queue.length) { return; }
711
709
  var qobj = this.queue[0];
712
- // 标记本次发送的队首请求(用于防止 timeout 与接收回包边界竞态导致双推进)
713
710
  qobj._done = false;
714
711
  qobj._txId = (++obj._txSeq);
715
712
  this.write(qobj.payload, qobj.cb);
716
713
  var msg = qobj.msg;
717
714
  var timeout = msg.timeout || responsetimeout;
718
- this.tout = setTimeout(function () {
719
- // 注意:回调内 this 不是 obj,统一使用 obj.tout
715
+ var maxPartialWait = msg.maxPartialWait || serialConfig.maxPartialWait || (timeout * 2);
716
+ var scheduleTimeout = function (delay) {
717
+ obj.tout = setTimeout(onResponseTimeout, delay);
718
+ };
719
+ var onResponseTimeout = function () {
720
720
  obj.tout = null;
721
721
 
722
- // 关键保护:timeout 触发时,只允许处理“仍然挂在队首的同一个 qobj”。
723
- // 若队列已因正常回包 dequeue 推进,则该 timeout 已失效,直接忽略,避免连续发送/错位。
722
+ // 只处理仍然挂在队首的同一个请求;正常回包已推进队列时,该 timeout 失效。
724
723
  if (!obj.queue || !obj.queue.length) { return; }
725
724
  if (obj.queue[0] !== qobj) { return; }
726
725
  if (qobj._done) { return; }
726
+
727
+ if (spliton === "frame" && obj._framePending && obj._framePending.proto === '698-len') {
728
+ var now = Date.now();
729
+ var startedAt = obj._framePending.since || now;
730
+ var waited = now - startedAt;
731
+ if (waited < maxPartialWait) {
732
+ scheduleTimeout(Math.max(50, Math.min(timeout, maxPartialWait - waited)));
733
+ return;
734
+ }
735
+ }
736
+
727
737
  qobj._done = true;
728
738
 
729
- var msgout = obj.dequeue() || {};
739
+ var msgout = obj.dequeue(true) || {};
730
740
  msgout.port = id;
731
741
  // // if we have some leftover stuff, just send it
732
742
  // if (i !== 0) {
@@ -753,8 +763,11 @@ module.exports = function (RED) {
753
763
  if (m) {
754
764
  if (spliton === "frame") {
755
765
  const mbuf = Buffer.from(m);
756
- msgout.payload = mbuf;
757
- msgout.payload_hex = mbuf.toString('hex').toUpperCase();
766
+ msgout.partial_payload = mbuf;
767
+ msgout.partial_payload_hex = mbuf.toString('hex').toUpperCase();
768
+ msgout.payload = Buffer.alloc(0);
769
+ msgout.payload_hex = "";
770
+ msgout.reason = "FRAME_TIMEOUT_PARTIAL";
758
771
  } else {
759
772
  if (binoutput !== "bin") { m = m.toString(); }
760
773
  msgout.payload = m;
@@ -768,25 +781,27 @@ module.exports = function (RED) {
768
781
  }
769
782
  }
770
783
  msgout.status = "ERR_TIMEOUT";
784
+ obj._framePending = null;
771
785
  /* Notify the sender that a timeout occurred */
772
786
  obj._emitter.emit('timeout', msgout, qobj.sender);
773
- }, timeout);
787
+ };
788
+ scheduleTimeout(timeout);
774
789
  },
775
- dequeue: function () {
790
+ dequeue: function (ignoreRxTokenGuard) {
776
791
  // if we are trying to dequeue stuff from an
777
792
  // empty queue, that's an unsolicited message
778
793
  if (!this.queue.length) { return null; }
779
794
 
780
- // 关键保护:同一次 serial 'data' 回调(同 _rxToken)内只允许 dequeue 一次。
781
- // 防止 frame 自动解析在同一批输入字节上被多协议/多分支误判两次,导致队列被推进两次(连续发送)。
782
- if (typeof this._rxToken === 'number' && this._rxDequeuedToken === this._rxToken) {
783
- return null;
784
- }
785
- if (typeof this._rxToken === 'number') {
786
- this._rxDequeuedToken = this._rxToken;
795
+ // 同一次 serial 'data' 回调内只允许一次出队,避免粘包/误判让队列连续推进。
796
+ if (!ignoreRxTokenGuard) {
797
+ if (typeof this._rxToken === 'number' && this._rxDequeuedToken === this._rxToken) {
798
+ return null;
799
+ }
800
+ if (typeof this._rxToken === 'number') {
801
+ this._rxDequeuedToken = this._rxToken;
802
+ }
787
803
  }
788
804
 
789
- // 出队前标记 done:用于 timeout 回调校验
790
805
  var qobj = this.queue[0];
791
806
  if (qobj && qobj._done !== true) { qobj._done = true; }
792
807
 
@@ -796,13 +811,10 @@ module.exports = function (RED) {
796
811
  request_msgid: msg._msgid,
797
812
  });
798
813
  delete msg.payload;
799
-
800
- // 取消当前请求的响应超时计时器
801
814
  if (obj.tout) {
802
815
  clearTimeout(obj.tout);
803
816
  obj.tout = null;
804
817
  }
805
-
806
818
  this.queue.shift();
807
819
  this.writehead();
808
820
  return msg;
@@ -996,8 +1008,7 @@ module.exports = function (RED) {
996
1008
  if (b.length >= 8 && b[7] !== 0x68) {
997
1009
  const n = b.indexOf(0x68, 1);
998
1010
  if (n > 0) return { ok: false, used: feCount + n, frame: input.slice(0, feCount + n), err: "645_SECOND_68_NOT_FOUND" };
999
- // 已经有足够字节判定形态错误,但又找不到下一个 0x68:同步点错位,丢 1 字节推进
1000
- return { ok: false, used: feCount + 1, frame: input.slice(0, feCount + 1), err: "645_BAD_SHAPE_DROP1" };
1011
+ return { ok: false };
1001
1012
  }
1002
1013
  if (b.length < 10) return { ok: false }; // 还缺 CTRL/LEN
1003
1014
 
@@ -1073,10 +1084,8 @@ module.exports = function (RED) {
1073
1084
 
1074
1085
  // 698(68-LEN 变体):68 LL LH C ... DATA ... FCS(2) 16 ;兼容 FE* 前导
1075
1086
  function tryParse698Len(input) {
1076
- // 统计 FE 前导(用于 used/frame 回到原始 input)
1077
- let feCount = 0;
1078
- while (feCount < input.length && input[feCount] === 0xFE) feCount++;
1079
- const b = input.slice(feCount);
1087
+ // 剥离前导 FE(注意:这里只做 698-LEN 侧的兼容;645 另有自己的 FE 处理)
1088
+ const b = stripFE(input);
1080
1089
  if (b.length < 6 || b[0] !== 0x68) return { ok: false };
1081
1090
 
1082
1091
  // ---- 读取长度域 ----
@@ -1107,12 +1116,10 @@ module.exports = function (RED) {
1107
1116
  return { ok: false };
1108
1117
  }
1109
1118
 
1110
-
1111
- // 长度上限:防止错位/噪声把 L 解读成极大值导致 assembleBuf 误判为半包长期等待
1112
- // 掉电次数等业务回包通常远小于 2KB;超出则高度可疑,丢 1 字节推进重同步
1119
+ // 长度上限:防止错位/噪声把 L 解读成极大值导致 assembleBuf 长期等待。
1113
1120
  const MAX_698_LEN_L = 2048;
1114
1121
  if (L > MAX_698_LEN_L) {
1115
- return { ok: false, used: feCount + 1, frame: input.slice(0, feCount + 1), err: "698_LEN_TOO_LARGE" };
1122
+ return { ok: false, used: 1, frame: input.slice(0, 1), err: "698_LEN_TOO_LARGE" };
1116
1123
  }
1117
1124
 
1118
1125
  // 约束 2:控制域 C 在 b[3],不可能是 0x16(结束符)
@@ -1120,13 +1127,10 @@ module.exports = function (RED) {
1120
1127
  if (b.length >= 4 && b[3] === 0x16) {
1121
1128
  return { ok: false };
1122
1129
  }
1123
- // 约束 3:控制域 C 必须看起来像 698 的控制域(功能码通常为 1=链路管理 或 3=用户数据)
1124
- // 目的:避免 645 的地址字节被当成 698 控制域,从而把 645 整帧误判为 698-LEN 候选帧并被消费掉。
1125
- // 698 控制域:bit0..bit3 为功能码(1 或 3 最常见);其他位为 DIR/PRM/分帧/扰码标志。
1130
+ // 约束 3:控制域功能码应为常见 698 值,避免把其它 0x68 开头数据误当成长 698 半包。
1126
1131
  const C = b[3] >>> 0;
1127
1132
  const func = C & 0x0F;
1128
1133
  if (!(func === 0x01 || func === 0x03)) {
1129
- // 不消费任何字节,让 645 解析器继续尝试
1130
1134
  return { ok: false };
1131
1135
  }
1132
1136
  // ------------------------------------------------------------------------
@@ -1136,13 +1140,22 @@ module.exports = function (RED) {
1136
1140
  // 你现有逻辑采用 expectedEnd = 1 + L,并要求 b[expectedEnd] == 0x16
1137
1141
  const expectedEnd = 1 + L;
1138
1142
 
1139
- // 半包:继续累积
1140
- if (b.length < expectedEnd + 1) return { ok: false };
1143
+ // 半包:继续累积。这里必须显式告诉主循环“这是 698-LEN 半包”,
1144
+ // 否则后续 645 parser 会把第一个 0x68 当作错位字节丢掉。
1145
+ if (b.length < expectedEnd + 1) {
1146
+ return {
1147
+ ok: false,
1148
+ pending: true,
1149
+ proto: '698-len',
1150
+ expected: expectedEnd + 1,
1151
+ have: b.length
1152
+ };
1153
+ }
1141
1154
 
1142
1155
  // 若 expectedEnd 位置不是 0x16,说明起点错位或存在脏字节
1143
1156
  // 这里消费 1 字节,避免死循环卡住
1144
1157
  if (b[expectedEnd] !== 0x16) {
1145
- return { ok: false, used: feCount + 1, frame: input.slice(0, feCount + 1), err: "698_LEN_BAD_END" };
1158
+ return { ok: false, used: 1, frame: input.slice(0, 1), err: "698_LEN_BAD_END" };
1146
1159
  }
1147
1160
 
1148
1161
  // ------------------------ FCS 校验(CRC-16/X.25) ------------------------
@@ -1160,14 +1173,12 @@ module.exports = function (RED) {
1160
1173
  const calcB = crc16x25(b, 1, expectedEnd - 2);
1161
1174
  const fcsOK = (calcA === fcs) || (calcB === fcs);
1162
1175
 
1163
- // 现场经常遇到:帧内容完整(68..16 边界正确),但 FCS 因链路噪声/串口转换器问题偶发不一致。
1164
- // 若此处直接判为错误帧并 dequeue,会导致上层“偶尔无法解码”(尤其是请求-响应严格匹配的场景)。
1165
- // 因此:当边界与长度域一致时,允许“容错通过”,并在 _meta 中标记 fcs_ok=false 供上层排障。
1176
+ // 边界完整但 FCS 不通过时上报错误帧;队首请求继续等待后续正确帧或最终超时。
1166
1177
  if (!fcsOK) {
1167
1178
  return {
1168
1179
  ok: false,
1169
- used: feCount + expectedEnd + 1,
1170
- frame: input.slice(0, feCount + expectedEnd + 1),
1180
+ used: expectedEnd + 1,
1181
+ frame: b.slice(0, expectedEnd + 1),
1171
1182
  fcs_ok: false,
1172
1183
  fcs_frame: fcs,
1173
1184
  fcs_calc_a: calcA,
@@ -1179,8 +1190,8 @@ module.exports = function (RED) {
1179
1190
  // FCS 通过:返回完整帧(含 0x68..0x16)
1180
1191
  return {
1181
1192
  ok: true,
1182
- used: feCount + expectedEnd + 1,
1183
- frame: input.slice(0, feCount + expectedEnd + 1),
1193
+ used: expectedEnd + 1,
1194
+ frame: b.slice(0, expectedEnd + 1),
1184
1195
  fcs_ok: true
1185
1196
  };
1186
1197
  }
@@ -1196,34 +1207,40 @@ module.exports = function (RED) {
1196
1207
  const rawFrame = b.slice(0, endPos + 1);
1197
1208
  const payloadEscaped = b.slice(1, endPos); // 去 7E
1198
1209
  const payload = unescapeHDLC(payloadEscaped);
1199
- // if (payload.length < 3) return { ok: false, used: endPos + 1, frame: rawFrame, err: "698_HDLC_TOO_SHORT" };
1200
- if (payload.length < 3) return { ok: false };
1201
- // 严格形态约束:698-HDLC 的“帧格式域”通常以 0xA0/0xA8/0xB0 等开头(高四位为 0xA 或 0xB)。
1202
- // 若不满足,极可能只是链路噪声/误同步的 0x7E,不应在 frame 模式下被消费掉(否则会 dequeue 错配)。
1203
- const fmt = payload[0] >>> 0;
1204
- const hi = fmt & 0xF0;
1205
- if (!(hi === 0xA0 || hi === 0xB0)) {
1206
- return { ok: false }; // 不消费,交给 698-LEN/645 再尝试
1207
- }
1210
+ if (payload.length < 3) return { ok: false, used: endPos + 1, frame: rawFrame, err: "698_HDLC_TOO_SHORT" };
1208
1211
  const fcsLo = payload[payload.length - 2];
1209
1212
  const fcsHi = payload[payload.length - 1];
1210
1213
  const fcs = (fcsHi << 8) | fcsLo;
1211
1214
  const calc = crc16x25(payload, 0, payload.length - 2);
1212
- // if (calc !== fcs) return { ok: false, used: endPos + 1, frame: rawFrame, err: "698_HDLC_CRC_FAIL" };
1213
- if (calc !== fcs) return { ok: false };
1215
+ if (calc !== fcs) return { ok: false, used: endPos + 1, frame: rawFrame, err: "698_HDLC_CRC_FAIL" };
1214
1216
  return { ok: true, used: endPos + 1, frame: rawFrame }; // 原始含 7E
1215
1217
  }
1216
1218
 
1217
1219
  // 统一喂入器:抽帧、报错、剔噪
1218
- let assembleBuf = Buffer.alloc(0);
1219
1220
  function feedAndExtract(d, emitOk, emitErr) {
1220
1221
  if (!Buffer.isBuffer(d)) d = Buffer.from(d);
1221
1222
  assembleBuf = Buffer.concat([assembleBuf, d]);
1222
1223
 
1223
- // 修复:部分现场链路/中间层会把原始 8-bit 串口数据“扩展”为 UTF-16LE 形态
1224
- // (如 0x7E -> 0x7E00 0x7E00...),导致帧头无法识别。
1225
- // 需要自动识别并还原原始 8-bit 数据。
1226
- // (已移除 normalizeUtf16Interleave 修复块)
1224
+ // 防溢出
1225
+ if (assembleBuf.length > bufMaxSize) {
1226
+ emitErr(Buffer.from(assembleBuf), "BUFFER_OVERFLOW_DROP_OLD");
1227
+ assembleBuf = Buffer.alloc(0);
1228
+ obj._framePending = null;
1229
+ return;
1230
+ }
1231
+
1232
+ // 前导剔噪:仅保留以 FE/68/7E 开头;若以 FE 开头,剥离所有 FE 前导
1233
+ let s = 0;
1234
+ while (s < assembleBuf.length) {
1235
+ const c = assembleBuf[s];
1236
+ if (c === 0xFE || c === 0x68 || c === 0x7E) break;
1237
+ s++;
1238
+ }
1239
+ if (s > 0) assembleBuf = assembleBuf.slice(s);
1240
+ if (assembleBuf.length && assembleBuf[0] === 0xFE) {
1241
+ let k = 0; while (k < assembleBuf.length && assembleBuf[k] === 0xFE) k++;
1242
+ assembleBuf = assembleBuf.slice(k);
1243
+ }
1227
1244
 
1228
1245
  // 抽帧
1229
1246
  while (assembleBuf.length >= 5) {
@@ -1236,21 +1253,17 @@ module.exports = function (RED) {
1236
1253
  // 关键:698-LEN 必须优先于 645,否则遇到 698 帧内出现 0x68(如示例报文第7字节)会被 645 误判并卡住
1237
1254
  let r = tryParseBleGNW(assembleBuf);
1238
1255
  if (!r.ok) r = tryParse698HDLC(assembleBuf);
1239
-
1240
- // ---- 兜底:0x7E 起始已形成候选段(存在第二个 0x7E),但 BLE/HDLC 均无法校验通过 -> 丢 1 字节推进重同步 ----
1241
- // 仅在候选段“闭合”时执行(有第二个 0x7E),避免误伤半包;并要求 endPos>=5,避免误伤 7E7E/7E7E7E 前导
1242
- if (!r.ok && assembleBuf.length && assembleBuf[0] === 0x7E) {
1243
- const endPos = assembleBuf.indexOf(0x7E, 1);
1244
- if (endPos >= 5) {
1245
- r = { ok: false, used: 1, frame: assembleBuf.slice(0, 1), err: "DROP_7E_BADFRAME" };
1246
- }
1247
- }
1248
-
1249
-
1250
1256
  if (!r.ok) r = tryParse698Len(assembleBuf);
1257
+ if (!r.ok && r.pending) {
1258
+ obj._framePending = Object.assign({}, r, {
1259
+ since: (obj._framePending && obj._framePending.proto === r.proto) ? obj._framePending.since : Date.now()
1260
+ });
1261
+ break;
1262
+ }
1251
1263
  if (!r.ok) r = tryParse645(assembleBuf);
1252
1264
 
1253
1265
  if (r.ok) {
1266
+ obj._framePending = null;
1254
1267
  // 附加解析元数据(例如 698 FCS 校验结果),供上层排障
1255
1268
  if (r && typeof r === 'object') {
1256
1269
  try {
@@ -1271,14 +1284,10 @@ module.exports = function (RED) {
1271
1284
  continue;
1272
1285
  }
1273
1286
  if (r.used) {
1274
- // resync/drop1 类(通常 used 很小)不应上报 ERR_FRAME,否则会产生大量重复输出
1287
+ obj._framePending = null;
1275
1288
  const used = r.used >>> 0;
1276
1289
  const frameBuf = r.frame ? Buffer.from(r.frame) : null;
1277
1290
  const frameLen = frameBuf ? frameBuf.length : 0;
1278
-
1279
- // 判定是否为“仅推进同步点”的消费:
1280
- // - used == 1 或 frameLen <= 1:典型 drop1
1281
- // - 或 err 属于已知的 resync 类原因
1282
1291
  const errCode = (r && r.err) ? String(r.err) : "";
1283
1292
  const isResyncDrop = (used <= 1) || (frameLen <= 1) ||
1284
1293
  errCode === "DROP_7E_BADFRAME" ||
@@ -1288,7 +1297,7 @@ module.exports = function (RED) {
1288
1297
  errCode === "645_SECOND_68_NOT_FOUND";
1289
1298
 
1290
1299
  if (!isResyncDrop) {
1291
- // 只有“闭合候选帧但校验/结构失败”的情况才上报 ERR_FRAME
1300
+ obj._framePending = null;
1292
1301
  try {
1293
1302
  if (r.frame) {
1294
1303
  r.frame._meta = Object.assign({}, r.frame._meta || {}, {
@@ -1301,12 +1310,11 @@ module.exports = function (RED) {
1301
1310
  });
1302
1311
  }
1303
1312
  } catch (e) {
1304
- // ignore
1313
+ // ignore:诊断信息不能影响主流程
1305
1314
  }
1306
1315
  emitErr(r.frame, errCode || "FRAME_INVALID");
1307
1316
  }
1308
1317
 
1309
- // 无论是否上报,都必须消费,防止 assembleBuf 卡死
1310
1318
  assembleBuf = assembleBuf.slice(used);
1311
1319
  continue;
1312
1320
  }
@@ -1318,7 +1326,6 @@ module.exports = function (RED) {
1318
1326
 
1319
1327
 
1320
1328
  obj.serial.on('data', function (d) {
1321
- // 标记本次接收事件 token(用于防止同一次 data 回调内重复 dequeue 推进队列)
1322
1329
  obj._rxToken = (obj._rxToken || 0) + 1;
1323
1330
  // RED.log.info("data::::" + d);
1324
1331
  function emitData(data) {
@@ -1365,17 +1372,15 @@ module.exports = function (RED) {
1365
1372
  function (badBuf, reason) {
1366
1373
  // 错误帧同样保持 Buffer 输出,并提供 HEX 字符串,便于定位截断点/前导位置
1367
1374
  var m = Buffer.from(badBuf);
1368
- var last_sender = null;
1369
- if (obj.queue.length) { last_sender = obj.queue[0].sender; }
1370
- // 关键修复:错误帧(CRC/CS/结尾不符)不应 dequeue,否则会把“请求上下文”弹出队列,导致后续正确回包无法匹配
1371
- var msgout = (obj.queue && obj.queue.length) ? Object.assign({}, obj.queue[0].msg) : {};
1375
+ var msgout = {};
1372
1376
  msgout.payload = m;
1373
1377
  msgout.payload_hex = m.toString('hex').toUpperCase();
1374
1378
  msgout.port = port;
1375
1379
  msgout.status = "ERR_FRAME";
1376
1380
  msgout.reason = reason || "FRAME_INVALID";
1377
1381
  msgout.is_unsolicited = !(obj.queue && obj.queue.length);
1378
- obj._emitter.emit('data', msgout, last_sender);
1382
+ // 错误帧不绑定请求 sender,也不 dequeue;队首请求继续等完整帧或超时。
1383
+ obj._emitter.emit('data', msgout, null);
1379
1384
  }
1380
1385
  );
1381
1386
  return; // 已处理