zwave-js 11.0.0 → 11.2.0

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 (158) hide show
  1. package/build/lib/controller/Controller.d.ts +86 -3
  2. package/build/lib/controller/Controller.d.ts.map +1 -1
  3. package/build/lib/controller/Controller.js +391 -164
  4. package/build/lib/controller/Controller.js.map +1 -1
  5. package/build/lib/controller/Features.js +1 -1
  6. package/build/lib/controller/Features.js.map +1 -1
  7. package/build/lib/controller/Inclusion.js +6 -6
  8. package/build/lib/controller/Inclusion.js.map +1 -1
  9. package/build/lib/controller/MockControllerBehaviors.d.ts.map +1 -1
  10. package/build/lib/controller/MockControllerBehaviors.js +3 -2
  11. package/build/lib/controller/MockControllerBehaviors.js.map +1 -1
  12. package/build/lib/controller/MockControllerState.js +2 -2
  13. package/build/lib/controller/MockControllerState.js.map +1 -1
  14. package/build/lib/controller/NodeInformationFrame.d.ts.map +1 -1
  15. package/build/lib/controller/NodeInformationFrame.js +6 -1
  16. package/build/lib/controller/NodeInformationFrame.js.map +1 -1
  17. package/build/lib/controller/ZWaveSDKVersions.js +1 -1
  18. package/build/lib/controller/ZWaveSDKVersions.js.map +1 -1
  19. package/build/lib/controller/_Types.js +1 -1
  20. package/build/lib/controller/_Types.js.map +1 -1
  21. package/build/lib/driver/Bootloader.js +1 -1
  22. package/build/lib/driver/Bootloader.js.map +1 -1
  23. package/build/lib/driver/Driver.d.ts +44 -12
  24. package/build/lib/driver/Driver.d.ts.map +1 -1
  25. package/build/lib/driver/Driver.js +495 -207
  26. package/build/lib/driver/Driver.js.map +1 -1
  27. package/build/lib/driver/MessageGenerators.d.ts.map +1 -1
  28. package/build/lib/driver/MessageGenerators.js +4 -11
  29. package/build/lib/driver/MessageGenerators.js.map +1 -1
  30. package/build/lib/driver/NetworkCache.d.ts +5 -0
  31. package/build/lib/driver/NetworkCache.d.ts.map +1 -1
  32. package/build/lib/driver/NetworkCache.js +5 -0
  33. package/build/lib/driver/NetworkCache.js.map +1 -1
  34. package/build/lib/driver/SerialAPICommandMachine.d.ts +17 -11
  35. package/build/lib/driver/SerialAPICommandMachine.d.ts.map +1 -1
  36. package/build/lib/driver/SerialAPICommandMachine.js +16 -6
  37. package/build/lib/driver/SerialAPICommandMachine.js.map +1 -1
  38. package/build/lib/driver/StateMachineShared.d.ts +27 -52
  39. package/build/lib/driver/StateMachineShared.d.ts.map +1 -1
  40. package/build/lib/driver/StateMachineShared.js +16 -48
  41. package/build/lib/driver/StateMachineShared.js.map +1 -1
  42. package/build/lib/node/Node.d.ts +11 -0
  43. package/build/lib/node/Node.d.ts.map +1 -1
  44. package/build/lib/node/Node.js +223 -8
  45. package/build/lib/node/Node.js.map +1 -1
  46. package/build/lib/node/NodeReadyMachine.d.ts +2 -2
  47. package/build/lib/node/NodeReadyMachine.d.ts.map +1 -1
  48. package/build/lib/node/NodeReadyMachine.js.map +1 -1
  49. package/build/lib/node/NodeStatusMachine.d.ts +2 -2
  50. package/build/lib/node/NodeStatusMachine.d.ts.map +1 -1
  51. package/build/lib/node/NodeStatusMachine.js.map +1 -1
  52. package/build/lib/serialapi/_Types.js +1 -1
  53. package/build/lib/serialapi/_Types.js.map +1 -1
  54. package/build/lib/serialapi/application/ApplicationCommandRequest.js +3 -4
  55. package/build/lib/serialapi/application/ApplicationCommandRequest.js.map +1 -1
  56. package/build/lib/serialapi/application/ApplicationUpdateRequest.js +13 -21
  57. package/build/lib/serialapi/application/ApplicationUpdateRequest.js.map +1 -1
  58. package/build/lib/serialapi/application/BridgeApplicationCommandRequest.js +2 -3
  59. package/build/lib/serialapi/application/BridgeApplicationCommandRequest.js.map +1 -1
  60. package/build/lib/serialapi/application/SerialAPIStartedRequest.js +3 -4
  61. package/build/lib/serialapi/application/SerialAPIStartedRequest.js.map +1 -1
  62. package/build/lib/serialapi/application/ShutdownMessages.js +4 -6
  63. package/build/lib/serialapi/application/ShutdownMessages.js.map +1 -1
  64. package/build/lib/serialapi/capability/GetControllerCapabilitiesMessages.js +4 -6
  65. package/build/lib/serialapi/capability/GetControllerCapabilitiesMessages.js.map +1 -1
  66. package/build/lib/serialapi/capability/GetControllerVersionMessages.js +4 -6
  67. package/build/lib/serialapi/capability/GetControllerVersionMessages.js.map +1 -1
  68. package/build/lib/serialapi/capability/GetProtocolVersionMessages.js +4 -6
  69. package/build/lib/serialapi/capability/GetProtocolVersionMessages.js.map +1 -1
  70. package/build/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.js +4 -6
  71. package/build/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.js.map +1 -1
  72. package/build/lib/serialapi/capability/GetSerialApiInitDataMessages.js +4 -6
  73. package/build/lib/serialapi/capability/GetSerialApiInitDataMessages.js.map +1 -1
  74. package/build/lib/serialapi/capability/HardResetRequest.js +4 -6
  75. package/build/lib/serialapi/capability/HardResetRequest.js.map +1 -1
  76. package/build/lib/serialapi/capability/SerialAPISetupMessages.js +59 -88
  77. package/build/lib/serialapi/capability/SerialAPISetupMessages.js.map +1 -1
  78. package/build/lib/serialapi/capability/SetApplicationNodeInformationRequest.js +2 -3
  79. package/build/lib/serialapi/capability/SetApplicationNodeInformationRequest.js.map +1 -1
  80. package/build/lib/serialapi/memory/GetControllerIdMessages.js +4 -6
  81. package/build/lib/serialapi/memory/GetControllerIdMessages.js.map +1 -1
  82. package/build/lib/serialapi/misc/GetBackgroundRSSIMessages.js +4 -6
  83. package/build/lib/serialapi/misc/GetBackgroundRSSIMessages.js.map +1 -1
  84. package/build/lib/serialapi/misc/SetRFReceiveModeMessages.js +4 -6
  85. package/build/lib/serialapi/misc/SetRFReceiveModeMessages.js.map +1 -1
  86. package/build/lib/serialapi/misc/SetSerialApiTimeoutsMessages.js +4 -6
  87. package/build/lib/serialapi/misc/SetSerialApiTimeoutsMessages.js.map +1 -1
  88. package/build/lib/serialapi/misc/SoftResetRequest.js +2 -3
  89. package/build/lib/serialapi/misc/SoftResetRequest.js.map +1 -1
  90. package/build/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.js +6 -8
  91. package/build/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.js.map +1 -1
  92. package/build/lib/serialapi/network-mgmt/AssignPriorityReturnRouteMessages.js +6 -9
  93. package/build/lib/serialapi/network-mgmt/AssignPriorityReturnRouteMessages.js.map +1 -1
  94. package/build/lib/serialapi/network-mgmt/AssignPrioritySUCReturnRouteMessages.js +6 -9
  95. package/build/lib/serialapi/network-mgmt/AssignPrioritySUCReturnRouteMessages.js.map +1 -1
  96. package/build/lib/serialapi/network-mgmt/AssignReturnRouteMessages.js +6 -9
  97. package/build/lib/serialapi/network-mgmt/AssignReturnRouteMessages.js.map +1 -1
  98. package/build/lib/serialapi/network-mgmt/AssignSUCReturnRouteMessages.js +6 -9
  99. package/build/lib/serialapi/network-mgmt/AssignSUCReturnRouteMessages.js.map +1 -1
  100. package/build/lib/serialapi/network-mgmt/DeleteReturnRouteMessages.js +6 -9
  101. package/build/lib/serialapi/network-mgmt/DeleteReturnRouteMessages.js.map +1 -1
  102. package/build/lib/serialapi/network-mgmt/DeleteSUCReturnRouteMessages.js +6 -9
  103. package/build/lib/serialapi/network-mgmt/DeleteSUCReturnRouteMessages.js.map +1 -1
  104. package/build/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.js +4 -6
  105. package/build/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.js.map +1 -1
  106. package/build/lib/serialapi/network-mgmt/GetPriorityRouteMessages.js +4 -6
  107. package/build/lib/serialapi/network-mgmt/GetPriorityRouteMessages.js.map +1 -1
  108. package/build/lib/serialapi/network-mgmt/GetRoutingInfoMessages.js +4 -6
  109. package/build/lib/serialapi/network-mgmt/GetRoutingInfoMessages.js.map +1 -1
  110. package/build/lib/serialapi/network-mgmt/GetSUCNodeIdMessages.js +4 -6
  111. package/build/lib/serialapi/network-mgmt/GetSUCNodeIdMessages.js.map +1 -1
  112. package/build/lib/serialapi/network-mgmt/IsFailedNodeMessages.js +4 -6
  113. package/build/lib/serialapi/network-mgmt/IsFailedNodeMessages.js.map +1 -1
  114. package/build/lib/serialapi/network-mgmt/RemoveFailedNodeMessages.js +8 -11
  115. package/build/lib/serialapi/network-mgmt/RemoveFailedNodeMessages.js.map +1 -1
  116. package/build/lib/serialapi/network-mgmt/RemoveNodeFromNetworkRequest.js +6 -8
  117. package/build/lib/serialapi/network-mgmt/RemoveNodeFromNetworkRequest.js.map +1 -1
  118. package/build/lib/serialapi/network-mgmt/ReplaceFailedNodeRequest.js +8 -11
  119. package/build/lib/serialapi/network-mgmt/ReplaceFailedNodeRequest.js.map +1 -1
  120. package/build/lib/serialapi/network-mgmt/RequestNodeInfoMessages.js +4 -6
  121. package/build/lib/serialapi/network-mgmt/RequestNodeInfoMessages.js.map +1 -1
  122. package/build/lib/serialapi/network-mgmt/RequestNodeNeighborUpdateMessages.js +5 -7
  123. package/build/lib/serialapi/network-mgmt/RequestNodeNeighborUpdateMessages.js.map +1 -1
  124. package/build/lib/serialapi/network-mgmt/SetPriorityRouteMessages.js +4 -6
  125. package/build/lib/serialapi/network-mgmt/SetPriorityRouteMessages.js.map +1 -1
  126. package/build/lib/serialapi/network-mgmt/SetSUCNodeIDMessages.js +7 -10
  127. package/build/lib/serialapi/network-mgmt/SetSUCNodeIDMessages.js.map +1 -1
  128. package/build/lib/serialapi/nvm/ExtNVMReadLongBufferMessages.js +4 -6
  129. package/build/lib/serialapi/nvm/ExtNVMReadLongBufferMessages.js.map +1 -1
  130. package/build/lib/serialapi/nvm/ExtNVMReadLongByteMessages.js +4 -6
  131. package/build/lib/serialapi/nvm/ExtNVMReadLongByteMessages.js.map +1 -1
  132. package/build/lib/serialapi/nvm/ExtNVMWriteLongBufferMessages.js +4 -6
  133. package/build/lib/serialapi/nvm/ExtNVMWriteLongBufferMessages.js.map +1 -1
  134. package/build/lib/serialapi/nvm/ExtNVMWriteLongByteMessages.js +4 -6
  135. package/build/lib/serialapi/nvm/ExtNVMWriteLongByteMessages.js.map +1 -1
  136. package/build/lib/serialapi/nvm/FirmwareUpdateNVMMessages.js +29 -43
  137. package/build/lib/serialapi/nvm/FirmwareUpdateNVMMessages.js.map +1 -1
  138. package/build/lib/serialapi/nvm/GetNVMIdMessages.js +6 -8
  139. package/build/lib/serialapi/nvm/GetNVMIdMessages.js.map +1 -1
  140. package/build/lib/serialapi/nvm/NVMOperationsMessages.js +6 -8
  141. package/build/lib/serialapi/nvm/NVMOperationsMessages.js.map +1 -1
  142. package/build/lib/serialapi/transport/SendDataBridgeMessages.js +12 -18
  143. package/build/lib/serialapi/transport/SendDataBridgeMessages.js.map +1 -1
  144. package/build/lib/serialapi/transport/SendDataMessages.js +14 -21
  145. package/build/lib/serialapi/transport/SendDataMessages.js.map +1 -1
  146. package/package.json +17 -16
  147. package/build/lib/driver/CommandQueueMachine.d.ts +0 -60
  148. package/build/lib/driver/CommandQueueMachine.d.ts.map +0 -1
  149. package/build/lib/driver/CommandQueueMachine.js +0 -259
  150. package/build/lib/driver/CommandQueueMachine.js.map +0 -1
  151. package/build/lib/driver/SendThreadMachine.d.ts +0 -97
  152. package/build/lib/driver/SendThreadMachine.d.ts.map +0 -1
  153. package/build/lib/driver/SendThreadMachine.js +0 -286
  154. package/build/lib/driver/SendThreadMachine.js.map +0 -1
  155. package/build/lib/driver/TransactionMachine.d.ts +0 -30
  156. package/build/lib/driver/TransactionMachine.d.ts.map +0 -1
  157. package/build/lib/driver/TransactionMachine.js +0 -250
  158. package/build/lib/driver/TransactionMachine.js.map +0 -1
@@ -36,6 +36,7 @@ const serial_1 = require("@zwave-js/serial");
36
36
  const shared_1 = require("@zwave-js/shared");
37
37
  const async_1 = require("alcalzone-shared/async");
38
38
  const deferred_promise_1 = require("alcalzone-shared/deferred-promise");
39
+ const sorted_list_1 = require("alcalzone-shared/sorted-list");
39
40
  const typeguards_1 = require("alcalzone-shared/typeguards");
40
41
  const crypto_1 = require("crypto");
41
42
  const fs_extra_1 = __importDefault(require("fs-extra"));
@@ -62,7 +63,8 @@ const statistics_1 = require("../telemetry/statistics");
62
63
  const Bootloader_1 = require("./Bootloader");
63
64
  const MessageGenerators_1 = require("./MessageGenerators");
64
65
  const NetworkCache_1 = require("./NetworkCache");
65
- const SendThreadMachine_1 = require("./SendThreadMachine");
66
+ const SerialAPICommandMachine_1 = require("./SerialAPICommandMachine");
67
+ const StateMachineShared_1 = require("./StateMachineShared");
66
68
  const ThrottlePresets_1 = require("./ThrottlePresets");
67
69
  const Transaction_1 = require("./Transaction");
68
70
  const TransportServiceMachine_1 = require("./TransportServiceMachine");
@@ -188,13 +190,16 @@ class Driver extends shared_1.TypedEventEmitter {
188
190
  constructor(port, options) {
189
191
  super();
190
192
  this.port = port;
193
+ this.queuePaused = false;
191
194
  /** A map of handlers for all sorts of requests */
192
195
  this.requestHandlers = new Map();
193
- /** A map of awaited messages */
196
+ /** A list of awaited message headers */
197
+ this.awaitedMessageHeaders = [];
198
+ /** A list of awaited messages */
194
199
  this.awaitedMessages = [];
195
- /** A map of awaited commands */
200
+ /** A list of awaited commands */
196
201
  this.awaitedCommands = [];
197
- /** A map of awaited chunks from the bootloader */
202
+ /** A list of awaited chunks from the bootloader */
198
203
  this.awaitedBootloaderChunks = [];
199
204
  /** A map of Node ID -> ongoing sessions */
200
205
  this.nodeSessions = new Map();
@@ -268,9 +273,7 @@ class Driver extends shared_1.TypedEventEmitter {
268
273
  * Returns the next session ID for Transport Service CC
269
274
  */
270
275
  this.getNextTransportServiceSessionId = (0, shared_1.createWrappingCounter)(core_1.MAX_TRANSPORT_SERVICE_SESSION_ID, true);
271
- this.lastSaveToCache = 0;
272
- this.saveToCacheInterval = 150;
273
- this.isSavingToCache = false;
276
+ this.drainQueueBusy = false;
274
277
  this.sendNodeToSleepTimers = new Map();
275
278
  this._enteringBootloader = false;
276
279
  this.lastBackgroundRSSITimestamp = 0;
@@ -300,131 +303,8 @@ class Driver extends shared_1.TypedEventEmitter {
300
303
  logContainer: this._logContainer,
301
304
  deviceConfigPriorityDir: this._options.storage.deviceConfigPriorityDir,
302
305
  });
303
- // And initialize but don't start the send thread machine
304
- const sendThreadMachine = (0, SendThreadMachine_1.createSendThreadMachine)({
305
- sendData: this.writeSerial.bind(this),
306
- createSendDataAbort: () => new SendDataMessages_1.SendDataAbort(this),
307
- notifyUnsolicited: (msg) => {
308
- void this.handleUnsolicitedMessage(msg);
309
- },
310
- notifyRetry: (command, lastError, message, attempts, maxAttempts, delay) => {
311
- if (command === "SendData") {
312
- this.controllerLog.logNode(message.getNodeId() ?? 255, `did not respond after ${attempts}/${maxAttempts} attempts. Scheduling next try in ${delay} ms.`, "warn");
313
- }
314
- else {
315
- // Translate the error into a better one
316
- let errorReason;
317
- switch (lastError) {
318
- case "response timeout":
319
- errorReason = "No response from controller";
320
- this._controller?.incrementStatistics("timeoutResponse");
321
- break;
322
- case "callback timeout":
323
- errorReason = "No callback from controller";
324
- this._controller?.incrementStatistics("timeoutCallback");
325
- break;
326
- case "response NOK":
327
- errorReason =
328
- "The controller response indicated failure";
329
- break;
330
- case "callback NOK":
331
- errorReason =
332
- "The controller callback indicated failure";
333
- break;
334
- case "ACK timeout":
335
- this._controller?.incrementStatistics("timeoutACK");
336
- // fall through
337
- case "CAN":
338
- case "NAK":
339
- default:
340
- errorReason =
341
- "Failed to execute controller command";
342
- break;
343
- }
344
- this.controllerLog.print(`${errorReason} after ${attempts}/${maxAttempts} attempts. Scheduling next try in ${delay} ms.`, "warn");
345
- }
346
- },
347
- timestamp: core_1.highResTimestamp,
348
- rejectTransaction: (transaction, error) => {
349
- // If a node failed to respond in time, it might be sleeping
350
- if (this.isMissingNodeACK(transaction, error)) {
351
- if (this.handleMissingNodeACK(transaction))
352
- return;
353
- }
354
- // If the transaction was already started, we need to throw the error into the message generator
355
- // so it correctly gets ended. Otherwise just reject the result promise
356
- if (transaction.parts.self) {
357
- // eslint-disable-next-line @typescript-eslint/no-empty-function
358
- transaction.parts.self.throw(error).catch(() => { });
359
- }
360
- else {
361
- transaction.promise.reject(error);
362
- }
363
- },
364
- resolveTransaction: (transaction, result) => {
365
- // If the transaction was already started, we need to end the message generator early by throwing
366
- // the result. Otherwise just resolve the result promise
367
- if (transaction.parts.self) {
368
- // eslint-disable-next-line @typescript-eslint/no-empty-function
369
- transaction.parts.self.throw(result).catch(() => { });
370
- }
371
- else {
372
- transaction.promise.resolve(result);
373
- }
374
- },
375
- logOutgoingMessage: (msg) => {
376
- this.driverLog.logMessage(msg, {
377
- direction: "outbound",
378
- });
379
- if (process.env.NODE_ENV !== "test") {
380
- // Enrich error data in case something goes wrong
381
- Sentry.addBreadcrumb({
382
- category: "message",
383
- timestamp: Date.now() / 1000,
384
- type: "debug",
385
- data: {
386
- direction: "outbound",
387
- msgType: msg.type,
388
- functionType: msg.functionType,
389
- name: msg.constructor.name,
390
- nodeId: msg.getNodeId(),
391
- ...msg.toLogEntry(),
392
- },
393
- });
394
- }
395
- },
396
- log: this.driverLog.print.bind(this.driverLog),
397
- logQueue: this.driverLog.sendQueue.bind(this.driverLog),
398
- }, (0, shared_1.pick)(this._options, ["timeouts", "attempts"]));
399
- this.sendThread = (0, xstate_1.interpret)(sendThreadMachine);
306
+ this.queue = new sorted_list_1.SortedList();
400
307
  this._sendThreadIdle = false;
401
- this.sendThread.onTransition((state) => {
402
- if (state.changed) {
403
- this.sendThreadIdle = state.matches("idle");
404
- }
405
- });
406
- // For debugging
407
- // this.sendThread.onTransition((state) => {
408
- // if (state.changed)
409
- // this.driverLog.print(
410
- // `send thread state: ${state.toStrings().join("->")}`,
411
- // "verbose",
412
- // );
413
- // });
414
- // this.sendThread.onEvent((evt) => {
415
- // if (evt.type === "forward") {
416
- // this.driverLog.print(
417
- // // @ts-ignore
418
- // `forwarding event: ${evt.payload.type} from ${evt.from} to ${evt.to}`,
419
- // "verbose",
420
- // );
421
- // } else {
422
- // this.driverLog.print(
423
- // `send thread event: ${evt.type}`,
424
- // "verbose",
425
- // );
426
- // }
427
- // });
428
308
  }
429
309
  /** Whether the Send Thread is currently idle */
430
310
  get sendThreadIdle() {
@@ -684,7 +564,6 @@ class Driver extends shared_1.TypedEventEmitter {
684
564
  this.driverLog.print(`version ${exports.libVersion}`, "info");
685
565
  this.driverLog.print("", "info");
686
566
  this.driverLog.print("starting driver...");
687
- this.sendThread.start();
688
567
  // Open the serial port
689
568
  if (typeof this.port === "string") {
690
569
  if (this.port.startsWith("tcp://")) {
@@ -1077,7 +956,7 @@ class Driver extends shared_1.TypedEventEmitter {
1077
956
  if (!node.hasSUCReturnRoute &&
1078
957
  node.status !== _Types_1.NodeStatus.Dead) {
1079
958
  node.hasSUCReturnRoute =
1080
- await this.controller.assignSUCReturnRoute(node.id);
959
+ await this.controller.assignSUCReturnRoutes(node.id);
1081
960
  }
1082
961
  })();
1083
962
  }
@@ -1203,18 +1082,15 @@ class Driver extends shared_1.TypedEventEmitter {
1203
1082
  this.controllerLog.logNode(node.id, `The node is ${oldStatus === _Types_1.NodeStatus.Unknown ? "" : "now "}awake.`);
1204
1083
  // Make sure to handle the pending messages as quickly as possible
1205
1084
  if (oldStatus === _Types_1.NodeStatus.Asleep) {
1206
- this.sendThread.send({
1207
- type: "reduce",
1208
- reducer: ({ message }) => {
1209
- // Ignore messages that are not for this node
1210
- if (message.getNodeId() !== node.id)
1211
- return { type: "keep" };
1212
- // Resolve pings, so we don't need to send them (we know the node is awake)
1213
- if ((0, cc_1.messageIsPing)(message))
1214
- return { type: "resolve", message: undefined };
1215
- // Re-queue all other transactions for this node, so they get added in front of the others
1216
- return { type: "requeue" };
1217
- },
1085
+ this.reduceQueue(({ message }) => {
1086
+ // Ignore messages that are not for this node
1087
+ if (message.getNodeId() !== node.id)
1088
+ return { type: "keep" };
1089
+ // Resolve pings, so we don't need to send them (we know the node is awake)
1090
+ if ((0, cc_1.messageIsPing)(message))
1091
+ return { type: "resolve", message: undefined };
1092
+ // Re-queue all other transactions for this node, so they get added in front of the others
1093
+ return { type: "requeue" };
1218
1094
  });
1219
1095
  }
1220
1096
  }
@@ -1555,12 +1431,10 @@ class Driver extends shared_1.TypedEventEmitter {
1555
1431
  }
1556
1432
  /** Checks if there are any pending transactions that match the given predicate */
1557
1433
  hasPendingTransactions(predicate) {
1558
- const { queue, activeTransactions } = this.sendThread.state.context;
1559
- if (!!queue.find((t) => predicate(t)))
1434
+ if (!!this.queue.find((t) => predicate(t)))
1435
+ return true;
1436
+ if (this.currentTransaction && predicate(this.currentTransaction)) {
1560
1437
  return true;
1561
- for (const { transaction } of activeTransactions.values()) {
1562
- if (predicate(transaction))
1563
- return true;
1564
1438
  }
1565
1439
  return false;
1566
1440
  }
@@ -1753,7 +1627,7 @@ class Driver extends shared_1.TypedEventEmitter {
1753
1627
  // This is a bit hacky, but what the heck...
1754
1628
  if (!this._enteringBootloader) {
1755
1629
  // Resume sending
1756
- this.unpauseSendThread();
1630
+ this.unpauseSendQueue();
1757
1631
  // Soft-resetting disables any ongoing inclusion, so we need to reset
1758
1632
  // the state that is tracked in the controller
1759
1633
  this._controller?.setInclusionState(Inclusion_1.InclusionState.Idle);
@@ -1817,11 +1691,11 @@ class Driver extends shared_1.TypedEventEmitter {
1817
1691
  const pollController = async () => {
1818
1692
  try {
1819
1693
  // And resume sending - this requires us to unpause the send thread
1820
- this.unpauseSendThread();
1694
+ this.unpauseSendQueue();
1821
1695
  await this.sendMessage(new GetControllerVersionMessages_1.GetControllerVersionRequest(this), {
1822
1696
  supportCheck: false,
1823
1697
  });
1824
- this.pauseSendThread();
1698
+ this.pauseSendQueue();
1825
1699
  this.controllerLog.print("Serial API responded");
1826
1700
  return true;
1827
1701
  }
@@ -1929,8 +1803,9 @@ class Driver extends shared_1.TypedEventEmitter {
1929
1803
  this._destroyPromise = (0, deferred_promise_1.createDeferredPromise)();
1930
1804
  this.driverLog.print("destroying driver instance...");
1931
1805
  // First stop the send thread machine and close the serial port, so nothing happens anymore
1932
- if (this.sendThread.initialized)
1933
- this.sendThread.stop();
1806
+ if (this.serialAPIInterpreter?.status === xstate_1.InterpreterStatus.Running) {
1807
+ this.serialAPIInterpreter.stop();
1808
+ }
1934
1809
  if (this.serial != undefined) {
1935
1810
  // Avoid spewing errors if the port was in the middle of receiving something
1936
1811
  this.serial.removeAllListeners();
@@ -1959,7 +1834,6 @@ class Driver extends shared_1.TypedEventEmitter {
1959
1834
  }
1960
1835
  // Remove all timeouts
1961
1836
  for (const timeout of [
1962
- this.saveToCacheTimer,
1963
1837
  ...this.sendNodeToSleepTimers.values(),
1964
1838
  ...this.retryNodeInterviewTimeouts.values(),
1965
1839
  ...this.autoRefreshNodeValueTimers.values(),
@@ -1967,6 +1841,7 @@ class Driver extends shared_1.TypedEventEmitter {
1967
1841
  this.pollBackgroundRSSITimer,
1968
1842
  ...this.awaitedCommands.map((c) => c.timeout),
1969
1843
  ...this.awaitedMessages.map((m) => m.timeout),
1844
+ ...this.awaitedMessageHeaders.map((h) => h.timeout),
1970
1845
  ...this.awaitedBootloaderChunks.map((b) => b.timeout),
1971
1846
  ]) {
1972
1847
  if (timeout)
@@ -1992,16 +1867,25 @@ class Driver extends shared_1.TypedEventEmitter {
1992
1867
  switch (data) {
1993
1868
  // single-byte messages - just forward them to the send thread
1994
1869
  case serial_1.MessageHeaders.ACK: {
1995
- this.sendThread.send("ACK");
1870
+ if (this.serialAPIInterpreter?.status ===
1871
+ xstate_1.InterpreterStatus.Running) {
1872
+ this.serialAPIInterpreter.send("ACK");
1873
+ }
1996
1874
  return;
1997
1875
  }
1998
1876
  case serial_1.MessageHeaders.NAK: {
1999
- this.sendThread.send("NAK");
1877
+ if (this.serialAPIInterpreter?.status ===
1878
+ xstate_1.InterpreterStatus.Running) {
1879
+ this.serialAPIInterpreter.send("NAK");
1880
+ }
2000
1881
  this._controller?.incrementStatistics("NAK");
2001
1882
  return;
2002
1883
  }
2003
1884
  case serial_1.MessageHeaders.CAN: {
2004
- this.sendThread.send("CAN");
1885
+ if (this.serialAPIInterpreter?.status ===
1886
+ xstate_1.InterpreterStatus.Running) {
1887
+ this.serialAPIInterpreter.send("CAN");
1888
+ }
2005
1889
  this._controller?.incrementStatistics("CAN");
2006
1890
  return;
2007
1891
  }
@@ -2185,7 +2069,16 @@ class Driver extends shared_1.TypedEventEmitter {
2185
2069
  // We shouldn't throw just because logging a message fails
2186
2070
  this.driverLog.print(`Logging a message failed: ${(0, shared_1.getErrorMessage)(e)}`);
2187
2071
  }
2188
- this.sendThread.send({ type: "message", message: msg });
2072
+ // Check if this message is unsolicited by passing it to the Serial API command interpreter if possible
2073
+ if (this.serialAPIInterpreter?.status === xstate_1.InterpreterStatus.Running) {
2074
+ this.serialAPIInterpreter.send({
2075
+ type: "message",
2076
+ message: msg,
2077
+ });
2078
+ }
2079
+ else {
2080
+ void this.handleUnsolicitedMessage(msg);
2081
+ }
2189
2082
  }
2190
2083
  }
2191
2084
  /** Handles a decoding error and returns the desired reply to the stick */
@@ -2405,11 +2298,20 @@ class Driver extends shared_1.TypedEventEmitter {
2405
2298
  return false;
2406
2299
  }
2407
2300
  else if (node.canSleep) {
2301
+ if (node.status === _Types_1.NodeStatus.Asleep) {
2302
+ // We already moved the messages to the wakeup queue
2303
+ return true;
2304
+ }
2408
2305
  this.controllerLog.logNode(node.id, `${messagePart1}. It is probably asleep, moving its messages to the wakeup queue.`, "warn");
2409
- // Mark the node as asleep
2410
- // The handler for the asleep status will move the messages to the wakeup queue
2306
+ // There is no longer a reference to the current transaction. If it should be moved to the wakeup queue,
2307
+ // it temporarily needs to be added to the queue again.
2308
+ const handled = this.mayMoveToWakeupQueue(transaction);
2309
+ if (handled) {
2310
+ this.queue.add(transaction);
2311
+ }
2312
+ // Mark the node as asleep. This will move the messages to the wakeup queue
2411
2313
  node.markAsAsleep();
2412
- return this.mayMoveToWakeupQueue(transaction);
2314
+ return handled;
2413
2315
  }
2414
2316
  else {
2415
2317
  const errorMsg = `${messagePart1}, it is presumed dead`;
@@ -2692,6 +2594,10 @@ ${handlers.length} left`);
2692
2594
  this.controllerLog.logNode(cc.nodeId, `is unknown - discarding received command...`, "warn");
2693
2595
  return true;
2694
2596
  }
2597
+ // CRC16, Transport Service belong outside of Security encapsulation
2598
+ if (cc instanceof cc_1.CRC16CC || cc instanceof cc_1.TransportServiceCC) {
2599
+ return false;
2600
+ }
2695
2601
  if (cc.constructor.name.endsWith("Get") &&
2696
2602
  (cc.frameType === "multicast" || cc.frameType === "broadcast")) {
2697
2603
  this.controllerLog.logNode(cc.nodeId, `received GET-type command via ${cc.frameType} - discarding...`, "warn");
@@ -2797,23 +2703,18 @@ ${handlers.length} left`);
2797
2703
  }
2798
2704
  // It could also be that this is the node's response for a CC that we sent, but where the ACK is delayed
2799
2705
  if ((0, cc_1.isCommandClassContainer)(msg)) {
2800
- const { activeTransactions } = this.sendThread.state.context;
2801
- const pendingMessages = [...activeTransactions.values()]
2802
- .map((t) => t.transaction.getCurrentMessage())
2803
- .filter((m) => !!m);
2804
- const msgExpectingUpdate = pendingMessages.find((sentMsg) => {
2805
- return (sentMsg.expectsNodeUpdate() &&
2806
- sentMsg.isExpectedNodeUpdate(msg));
2807
- });
2808
- if (msgExpectingUpdate) {
2809
- // Found a message that is still in progress but expects this message in response.
2706
+ const currentMessage = this.currentTransaction?.getCurrentMessage();
2707
+ if (currentMessage &&
2708
+ currentMessage.expectsNodeUpdate() &&
2709
+ currentMessage.isExpectedNodeUpdate(msg)) {
2710
+ // The message we're currently sending is still in progress but expects this message in response.
2810
2711
  // Remember the message there.
2811
2712
  this.controllerLog.logNode(msg.getNodeId(), {
2812
2713
  message: `received expected response prematurely, remembering it...`,
2813
2714
  level: "verbose",
2814
2715
  direction: "inbound",
2815
2716
  });
2816
- msgExpectingUpdate.prematureNodeUpdate = msg;
2717
+ currentMessage.prematureNodeUpdate = msg;
2817
2718
  return;
2818
2719
  }
2819
2720
  }
@@ -3114,6 +3015,252 @@ ${handlers.length} left`);
3114
3015
  this._controller?.incrementStatistics("messagesTX");
3115
3016
  }
3116
3017
  }
3018
+ mayStartNextTransaction() {
3019
+ // We may not send anything if the send thread is paused
3020
+ if (this.queuePaused)
3021
+ return false;
3022
+ const nextTransaction = this.queue.peekStart();
3023
+ // We can't send anything if the queue is empty
3024
+ if (!nextTransaction)
3025
+ return false;
3026
+ const message = nextTransaction.message;
3027
+ const targetNode = message.getNodeUnsafe(this);
3028
+ // The transaction queue is sorted automatically. If the first message is for a sleeping node, all messages in the queue are.
3029
+ // There are a few exceptions:
3030
+ // 1. Pings may be used to determine whether a node is really asleep.
3031
+ // 2. Responses to nonce requests must be sent independent of the node status, because some sleeping nodes may try to send us encrypted messages.
3032
+ // If we don't send them, they block the send queue
3033
+ // 3. Nodes that can sleep but do not support wakeup: https://github.com/zwave-js/node-zwave-js/discussions/1537
3034
+ // We need to try and send messages to them even if they are asleep, because we might never hear from them
3035
+ // // While the queue is busy, we may not start any transaction, except nonce responses to the node we're currently communicating with
3036
+ // if (meta.state.matches("busy")) {
3037
+ // if (nextTransaction.priority === MessagePriority.Nonce) {
3038
+ // for (const active of ctx.activeTransactions.values()) {
3039
+ // if (
3040
+ // active.transaction.message.getNodeId() ===
3041
+ // nextTransaction.message.getNodeId()
3042
+ // ) {
3043
+ // return true;
3044
+ // }
3045
+ // }
3046
+ // }
3047
+ // return false;
3048
+ // }
3049
+ // Replies to nonce requests and Supervision Get requests must always be allowed
3050
+ if (nextTransaction.priority === core_1.MessagePriority.Nonce ||
3051
+ nextTransaction.priority === core_1.MessagePriority.Supervision) {
3052
+ return true;
3053
+ }
3054
+ // Same for pings
3055
+ if ((0, cc_1.messageIsPing)(message))
3056
+ return true;
3057
+ // Or messages to the controller
3058
+ if (!targetNode)
3059
+ return true;
3060
+ return (targetNode.status !== _Types_1.NodeStatus.Asleep ||
3061
+ (!targetNode.supportsCC(core_1.CommandClasses["Wake Up"]) &&
3062
+ targetNode.interviewStage >= _Types_1.InterviewStage.NodeInfo));
3063
+ }
3064
+ async drainQueue() {
3065
+ // Don't execute more than once at a time
3066
+ if (this.drainQueueBusy)
3067
+ return;
3068
+ this.drainQueueBusy = true;
3069
+ try {
3070
+ while (this.mayStartNextTransaction()) {
3071
+ const transaction = (this.currentTransaction =
3072
+ this.queue.shift());
3073
+ // We have something to send, so not idle
3074
+ this.sendThreadIdle = false;
3075
+ let error;
3076
+ try {
3077
+ await this.drainTransactionGenerator(transaction);
3078
+ }
3079
+ catch (e) {
3080
+ error = e;
3081
+ }
3082
+ finally {
3083
+ this.currentTransaction = undefined;
3084
+ }
3085
+ // Handle errors after clearing the current transaction.
3086
+ // Otherwise, it will get considered the active transaction and cause an unnecessary SendDataAbort
3087
+ if (error) {
3088
+ this.rejectTransaction(transaction, error);
3089
+ }
3090
+ }
3091
+ }
3092
+ finally {
3093
+ this.drainQueueBusy = false;
3094
+ this.sendThreadIdle = true;
3095
+ }
3096
+ // Avoid a deadlock when a transaction was added after the advanceQueue call,
3097
+ // but before setting the busy flag back to false
3098
+ if (this.mayStartNextTransaction())
3099
+ this.triggerQueue();
3100
+ }
3101
+ /** Steps through the message generator of a transaction. Throws an error if the transaction should fail. */
3102
+ async drainTransactionGenerator(transaction) {
3103
+ transaction.parts.start();
3104
+ // .self is now guaranteed to be defined
3105
+ let prevResult;
3106
+ // Step through the transaction as long as it gives us a next message
3107
+ while (!(await transaction.parts.self.next(prevResult)).done) {
3108
+ // The .current property of the current transactions's message generator
3109
+ // now contains the next message to send
3110
+ const msg = transaction.getCurrentMessage();
3111
+ // TODO: refactor this nested loop or make it part of executeSerialAPICommand
3112
+ attemptMessage: for (let attemptNumber = 1;; attemptNumber++) {
3113
+ try {
3114
+ prevResult = await this.executeSerialAPICommand(msg, transaction.stack);
3115
+ if ((0, SendDataShared_1.isTransmitReport)(prevResult) && !prevResult.isOK()) {
3116
+ // The node did not acknowledge the command. Convert this into an
3117
+ // error so it can be handled.
3118
+ // First throw into the generator, so it can be reset
3119
+ transaction.parts.self.throw(prevResult).catch(shared_1.noop);
3120
+ throw new core_1.ZWaveError("The node did not acknowledge the command", core_1.ZWaveErrorCodes.Controller_CallbackNOK, prevResult, transaction.stack);
3121
+ }
3122
+ // We got a result - it will be passed to the next iteration
3123
+ break attemptMessage;
3124
+ }
3125
+ catch (e) {
3126
+ let delay = 0;
3127
+ let zwError;
3128
+ if (!(0, core_1.isZWaveError)(e)) {
3129
+ zwError = (0, StateMachineShared_1.createMessageDroppedUnexpectedError)(e);
3130
+ }
3131
+ else {
3132
+ if (e.code === core_1.ZWaveErrorCodes.Controller_CommandAborted) {
3133
+ // This transaction was aborted by the driver due to a controller timeout.
3134
+ // Rejections, re-queuing etc. have been handled, so just drop it silently and
3135
+ // continue with the next message
3136
+ return;
3137
+ }
3138
+ else if ((0, SendDataShared_1.isSendData)(msg) &&
3139
+ e.code === core_1.ZWaveErrorCodes.Controller_Timeout &&
3140
+ e.context === "callback") {
3141
+ // If the callback to SendData times out, we need to issue a SendDataAbort
3142
+ await this.abortSendData();
3143
+ // Wait a short amount of time so everything can settle
3144
+ delay = 50;
3145
+ }
3146
+ if (this.mayRetrySerialAPICommand(msg, attemptNumber, e.code)) {
3147
+ // Retry the command
3148
+ if (delay)
3149
+ await (0, async_1.wait)(delay, true);
3150
+ continue attemptMessage;
3151
+ }
3152
+ zwError = e;
3153
+ }
3154
+ // Sending the command failed, reject the transaction
3155
+ throw zwError;
3156
+ }
3157
+ }
3158
+ }
3159
+ // This transaction is finished, try the next one
3160
+ }
3161
+ triggerQueue() {
3162
+ process.nextTick(() => this.drainQueue());
3163
+ }
3164
+ mayRetrySerialAPICommand(msg, attemptNumber, errorCode) {
3165
+ if (!(0, SendDataShared_1.isSendData)(msg))
3166
+ return false;
3167
+ if (msg instanceof SendDataMessages_1.SendDataMulticastRequest ||
3168
+ msg instanceof SendDataBridgeMessages_1.SendDataMulticastBridgeRequest) {
3169
+ // Don't try to resend multicast messages if they were already transmitted.
3170
+ // One or more nodes might have already reacted
3171
+ if (errorCode === core_1.ZWaveErrorCodes.Controller_CallbackNOK) {
3172
+ return false;
3173
+ }
3174
+ }
3175
+ return attemptNumber < msg.maxSendAttempts;
3176
+ }
3177
+ /**
3178
+ * Executes a Serial API command and returns or throws the result.
3179
+ * @param transaction The transaction which contains the message to be executed
3180
+ */
3181
+ async executeSerialAPICommand(msg, transactionSource) {
3182
+ const machine = (0, SerialAPICommandMachine_1.createSerialAPICommandMachine)(msg, {
3183
+ sendData: this.writeSerial.bind(this),
3184
+ notifyUnsolicited: (msg) => {
3185
+ void this.handleUnsolicitedMessage(msg);
3186
+ },
3187
+ notifyRetry: (lastError, message, attempts, maxAttempts, delay) => {
3188
+ // Translate the error into a better one
3189
+ let errorReason;
3190
+ switch (lastError) {
3191
+ case "response timeout":
3192
+ errorReason = "No response from controller";
3193
+ this._controller?.incrementStatistics("timeoutResponse");
3194
+ break;
3195
+ case "callback timeout":
3196
+ errorReason = "No callback from controller";
3197
+ this._controller?.incrementStatistics("timeoutCallback");
3198
+ break;
3199
+ case "response NOK":
3200
+ errorReason =
3201
+ "The controller response indicated failure";
3202
+ break;
3203
+ case "callback NOK":
3204
+ errorReason =
3205
+ "The controller callback indicated failure";
3206
+ break;
3207
+ case "ACK timeout":
3208
+ this._controller?.incrementStatistics("timeoutACK");
3209
+ // fall through
3210
+ case "CAN":
3211
+ case "NAK":
3212
+ default:
3213
+ errorReason =
3214
+ "Failed to execute controller command";
3215
+ break;
3216
+ }
3217
+ this.controllerLog.print(`${errorReason} after ${attempts}/${maxAttempts} attempts. Scheduling next try in ${delay} ms.`, "warn");
3218
+ },
3219
+ timestamp: core_1.highResTimestamp,
3220
+ logOutgoingMessage: (msg) => {
3221
+ this.driverLog.logMessage(msg, {
3222
+ direction: "outbound",
3223
+ });
3224
+ if (process.env.NODE_ENV !== "test") {
3225
+ // Enrich error data in case something goes wrong
3226
+ Sentry.addBreadcrumb({
3227
+ category: "message",
3228
+ timestamp: Date.now() / 1000,
3229
+ type: "debug",
3230
+ data: {
3231
+ direction: "outbound",
3232
+ msgType: msg.type,
3233
+ functionType: msg.functionType,
3234
+ name: msg.constructor.name,
3235
+ nodeId: msg.getNodeId(),
3236
+ ...msg.toLogEntry(),
3237
+ },
3238
+ });
3239
+ }
3240
+ },
3241
+ }, (0, shared_1.pick)(this._options, ["timeouts", "attempts"]));
3242
+ const result = (0, deferred_promise_1.createDeferredPromise)();
3243
+ this.serialAPIInterpreter = (0, xstate_1.interpret)(machine).onDone((evt) => {
3244
+ this.serialAPIInterpreter?.stop();
3245
+ this.serialAPIInterpreter = undefined;
3246
+ const cmdResult = evt.data;
3247
+ if (cmdResult.type === "success") {
3248
+ result.resolve(cmdResult.result);
3249
+ }
3250
+ else if (cmdResult.reason === "callback NOK" &&
3251
+ ((0, SendDataShared_1.isSendData)(msg) || (0, SendDataShared_1.isTransmitReport)(cmdResult.result))) {
3252
+ // For messages that were sent to a node, a NOK callback still contains useful info we need to evaluate
3253
+ // ... so we treat it as a result
3254
+ result.resolve(cmdResult.result);
3255
+ }
3256
+ else {
3257
+ // Convert to a Z-Wave error
3258
+ result.reject((0, StateMachineShared_1.serialAPICommandErrorToZWaveError)(cmdResult.reason, msg, cmdResult.result, transactionSource));
3259
+ }
3260
+ });
3261
+ this.serialAPIInterpreter.start();
3262
+ return result;
3263
+ }
3117
3264
  /**
3118
3265
  * Sends a message to the Z-Wave stick.
3119
3266
  * @param msg The message to send
@@ -3193,22 +3340,20 @@ ${handlers.length} left`);
3193
3340
  transaction.requestWakeUpOnDemand = !!options.requestWakeUpOnDemand;
3194
3341
  transaction.tag = options.tag;
3195
3342
  // And queue it
3196
- this.sendThread.send({ type: "add", transaction });
3343
+ this.queue.add(transaction);
3344
+ this.triggerQueue();
3197
3345
  // If the transaction should expire, start the timeout
3198
3346
  let expirationTimeout;
3199
3347
  if (options.expire) {
3200
3348
  expirationTimeout = setTimeout(() => {
3201
- this.sendThread.send({
3202
- type: "reduce",
3203
- reducer: (t) => {
3204
- if (t === transaction)
3205
- return {
3206
- type: "reject",
3207
- message: `The message has expired`,
3208
- code: core_1.ZWaveErrorCodes.Controller_MessageExpired,
3209
- };
3210
- return { type: "keep" };
3211
- },
3349
+ this.reduceQueue((t, _source) => {
3350
+ if (t === transaction)
3351
+ return {
3352
+ type: "reject",
3353
+ message: `The message has expired`,
3354
+ code: core_1.ZWaveErrorCodes.Controller_MessageExpired,
3355
+ };
3356
+ return { type: "keep" };
3212
3357
  });
3213
3358
  }, options.expire).unref();
3214
3359
  }
@@ -3420,6 +3565,27 @@ ${handlers.length} left`);
3420
3565
  // @ts-expect-error TS doesn't know we've narrowed the return type to match
3421
3566
  return this.sendCommandInternal(command, options);
3422
3567
  }
3568
+ async abortSendData(abortInterpreter = false) {
3569
+ try {
3570
+ const abort = new SendDataMessages_1.SendDataAbort(this);
3571
+ await this.writeSerial(abort.serialize());
3572
+ this.driverLog.logMessage(abort, {
3573
+ direction: "outbound",
3574
+ });
3575
+ // We're bypassing the serial API machine, so we need to wait for the ACK ourselves
3576
+ // This could also cause a NAK or CAN, but we don't really care
3577
+ await this.waitForMessageHeader(() => true, 500).catch(shared_1.noop);
3578
+ // Abort the currently active command machine only if the controller has timed out.
3579
+ // SendData commands we abort early MUST result in the normal callback.
3580
+ if (abortInterpreter &&
3581
+ this.serialAPIInterpreter?.status === xstate_1.InterpreterStatus.Running) {
3582
+ this.serialAPIInterpreter.send("abort");
3583
+ }
3584
+ }
3585
+ catch {
3586
+ // ignore
3587
+ }
3588
+ }
3423
3589
  /**
3424
3590
  * Sends a low-level message like ACK, NAK or CAN immediately
3425
3591
  * @param header The low-level message to send
@@ -3432,6 +3598,40 @@ ${handlers.length} left`);
3432
3598
  async writeSerial(data) {
3433
3599
  return this.serial?.writeAsync(data);
3434
3600
  }
3601
+ /**
3602
+ * Waits until a matching message header is received or a timeout has elapsed. Returns the received message.
3603
+ *
3604
+ * @param timeout The number of milliseconds to wait. If the timeout elapses, the returned promise will be rejected
3605
+ * @param predicate A predicate function to test all incoming message headers.
3606
+ */
3607
+ waitForMessageHeader(predicate, timeout) {
3608
+ return new Promise((resolve, reject) => {
3609
+ const promise = (0, deferred_promise_1.createDeferredPromise)();
3610
+ const entry = {
3611
+ predicate,
3612
+ handler: (msg) => promise.resolve(msg),
3613
+ timeout: undefined,
3614
+ };
3615
+ this.awaitedMessageHeaders.push(entry);
3616
+ const removeEntry = () => {
3617
+ if (entry.timeout)
3618
+ clearTimeout(entry.timeout);
3619
+ const index = this.awaitedMessageHeaders.indexOf(entry);
3620
+ if (index !== -1)
3621
+ this.awaitedMessageHeaders.splice(index, 1);
3622
+ };
3623
+ // When the timeout elapses, remove the wait entry and reject the returned Promise
3624
+ entry.timeout = setTimeout(() => {
3625
+ removeEntry();
3626
+ reject(new core_1.ZWaveError(`Received no matching serial frame within the provided timeout!`, core_1.ZWaveErrorCodes.Controller_Timeout));
3627
+ }, timeout);
3628
+ // When the promise is resolved, remove the wait entry and resolve the returned Promise
3629
+ void promise.then((cc) => {
3630
+ removeEntry();
3631
+ resolve(cc);
3632
+ });
3633
+ });
3634
+ }
3435
3635
  /**
3436
3636
  * Waits until an unsolicited serial message is received or a timeout has elapsed. Returns the received message.
3437
3637
  *
@@ -3524,6 +3724,31 @@ ${handlers.length} left`);
3524
3724
  unregister: removeEntry,
3525
3725
  };
3526
3726
  }
3727
+ rejectTransaction(transaction, error) {
3728
+ // If a node failed to respond in time, it might be sleeping
3729
+ if (this.isMissingNodeACK(transaction, error)) {
3730
+ if (this.handleMissingNodeACK(transaction))
3731
+ return;
3732
+ }
3733
+ // If the transaction was already started, we need to throw the error into the message generator
3734
+ // so it correctly gets ended. Otherwise just reject the result promise
3735
+ if (transaction.parts.self) {
3736
+ transaction.parts.self.throw(error).catch(shared_1.noop);
3737
+ }
3738
+ else {
3739
+ transaction.promise.reject(error);
3740
+ }
3741
+ }
3742
+ resolveTransaction(transaction, result) {
3743
+ // If the transaction was already started, we need to end the message generator early by throwing
3744
+ // the result. Otherwise just resolve the result promise
3745
+ if (transaction.parts.self) {
3746
+ transaction.parts.self.throw(result).catch(shared_1.noop);
3747
+ }
3748
+ else {
3749
+ transaction.promise.resolve(result);
3750
+ }
3751
+ }
3527
3752
  /** Checks if a message is allowed to go into the wakeup queue */
3528
3753
  mayMoveToWakeupQueue(transaction) {
3529
3754
  const msg = transaction.message;
@@ -3556,7 +3781,7 @@ ${handlers.length} left`);
3556
3781
  ...requeue,
3557
3782
  tag: "interview",
3558
3783
  };
3559
- const reducer = (transaction, _source) => {
3784
+ this.reduceQueue((transaction, _source) => {
3560
3785
  const msg = transaction.message;
3561
3786
  if (msg.getNodeId() !== nodeId)
3562
3787
  return { type: "keep" };
@@ -3567,16 +3792,14 @@ ${handlers.length} left`);
3567
3792
  ? requeueAndTagAsInterview
3568
3793
  : requeue
3569
3794
  : reject;
3570
- };
3571
- // Apply the reducer to the send thread
3572
- this.sendThread.send({ type: "reduce", reducer });
3795
+ });
3573
3796
  }
3574
3797
  /**
3575
3798
  * @internal
3576
3799
  * Rejects all pending transactions that match a predicate and removes them from the send queue
3577
3800
  */
3578
3801
  rejectTransactions(predicate, errorMsg = `The message has been removed from the queue`, errorCode = core_1.ZWaveErrorCodes.Controller_MessageDropped) {
3579
- const reducer = (transaction) => {
3802
+ this.reduceQueue((transaction, _source) => {
3580
3803
  if (predicate(transaction)) {
3581
3804
  return {
3582
3805
  type: "reject",
@@ -3587,9 +3810,7 @@ ${handlers.length} left`);
3587
3810
  else {
3588
3811
  return { type: "keep" };
3589
3812
  }
3590
- };
3591
- // Apply the reducer to the send thread
3592
- this.sendThread.send({ type: "reduce", reducer });
3813
+ });
3593
3814
  }
3594
3815
  /**
3595
3816
  * @internal
@@ -3599,22 +3820,89 @@ ${handlers.length} left`);
3599
3820
  this.rejectTransactions((t) => t.message.getNodeId() === nodeId, errorMsg, errorCode);
3600
3821
  }
3601
3822
  /**
3602
- * @internal
3603
- * Pauses the send thread, avoiding commands to be sent to the controller
3823
+ * Pauses the send queue, avoiding commands to be sent to the controller
3604
3824
  */
3605
- pauseSendThread() {
3606
- this.sendThread.send({ type: "pause" });
3825
+ pauseSendQueue() {
3826
+ this.queuePaused = true;
3607
3827
  }
3608
3828
  /**
3609
- * @internal
3610
- * Unpauses the send thread, allowing commands to be sent to the controller again
3829
+ * Unpauses the send queue, allowing commands to be sent to the controller again
3611
3830
  */
3612
- unpauseSendThread() {
3613
- this.sendThread.send({ type: "unpause" });
3831
+ unpauseSendQueue() {
3832
+ this.queuePaused = false;
3833
+ this.triggerQueue();
3834
+ }
3835
+ reduceQueue(reducer) {
3836
+ const dropQueued = [];
3837
+ let stopActive;
3838
+ const requeue = [];
3839
+ const reduceTransaction = (transaction, source) => {
3840
+ const reducerResult = reducer(transaction, source);
3841
+ switch (reducerResult.type) {
3842
+ case "drop":
3843
+ if (source === "queue") {
3844
+ dropQueued.push(transaction);
3845
+ }
3846
+ else {
3847
+ stopActive = transaction;
3848
+ }
3849
+ break;
3850
+ case "requeue":
3851
+ if (reducerResult.priority != undefined) {
3852
+ transaction.priority = reducerResult.priority;
3853
+ }
3854
+ if (reducerResult.tag != undefined) {
3855
+ transaction.tag = reducerResult.tag;
3856
+ }
3857
+ if (source === "active")
3858
+ stopActive = transaction;
3859
+ requeue.push(transaction);
3860
+ break;
3861
+ case "resolve":
3862
+ this.resolveTransaction(transaction, reducerResult.message);
3863
+ if (source === "queue") {
3864
+ dropQueued.push(transaction);
3865
+ }
3866
+ else {
3867
+ stopActive = transaction;
3868
+ }
3869
+ break;
3870
+ case "reject":
3871
+ this.rejectTransaction(transaction, new core_1.ZWaveError(reducerResult.message, reducerResult.code, undefined, transaction.stack));
3872
+ if (source === "queue") {
3873
+ dropQueued.push(transaction);
3874
+ }
3875
+ else {
3876
+ stopActive = transaction;
3877
+ }
3878
+ break;
3879
+ }
3880
+ };
3881
+ for (const transaction of this.queue) {
3882
+ reduceTransaction(transaction, "queue");
3883
+ }
3884
+ if (this.currentTransaction) {
3885
+ reduceTransaction(this.currentTransaction, "active");
3886
+ }
3887
+ // Now we know what to do with the transactions
3888
+ this.queue.remove(...dropQueued, ...requeue);
3889
+ this.queue.add(...requeue.map((t) => t.clone()));
3890
+ // Abort ongoing SendData messages that should be dropped
3891
+ if ((0, SendDataShared_1.isSendData)(stopActive?.message)) {
3892
+ void this.abortSendData();
3893
+ }
3894
+ // Continue sending
3895
+ this.triggerQueue();
3614
3896
  }
3615
- /** Re-sorts the send queue */
3616
- sortSendQueue() {
3617
- this.sendThread.send("sortQueue");
3897
+ /** @internal */
3898
+ resolvePendingPings(nodeId) {
3899
+ // When a previously sleeping node sends a NIF after a ping was sent to it, but not acknowledged yet,
3900
+ // the node is awake, but the ping would fail. Resolve pending pings, so communication can continue.
3901
+ const msg = this.currentTransaction?.parts.current;
3902
+ if (!!msg && (0, cc_1.messageIsPing)(msg) && msg.getNodeId() === nodeId) {
3903
+ // The pending transaction is a ping. Short-circuit its message generator by throwing something that's not an error
3904
+ this.currentTransaction?.parts.self.throw(undefined).catch(shared_1.noop);
3905
+ }
3618
3906
  }
3619
3907
  /**
3620
3908
  * @internal
@@ -3830,7 +4118,7 @@ ${handlers.length} left`);
3830
4118
  // Avoid re-transmissions etc. communicating with the bootloader
3831
4119
  this.rejectTransactions((_t) => true, "The controller is entering bootloader mode.");
3832
4120
  await this.trySoftReset();
3833
- this.pauseSendThread();
4121
+ this.pauseSendQueue();
3834
4122
  // Again, just to be very sure
3835
4123
  this.rejectTransactions((_t) => true, "The controller is entering bootloader mode.");
3836
4124
  // It would be nicer to not hardcode the command here, but since we're switching stream parsers
@@ -3880,7 +4168,7 @@ ${handlers.length} left`);
3880
4168
  });
3881
4169
  }
3882
4170
  else {
3883
- this.unpauseSendThread();
4171
+ this.unpauseSendQueue();
3884
4172
  await this.ensureSerialAPI();
3885
4173
  }
3886
4174
  }