zwave-js
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
689 lines • 37 kB
JavaScript
import { FirmwareDownloadStatus, FirmwareUpdateMetaDataCC, FirmwareUpdateMetaDataCCGet, FirmwareUpdateMetaDataCCReport, FirmwareUpdateMetaDataCCRequestReport, FirmwareUpdateMetaDataCCStatusReport, FirmwareUpdateMetaDataCCValues, FirmwareUpdateRequestStatus, FirmwareUpdateStatus, getEffectiveCCVersion, } from "@zwave-js/cc";
import { CRC16_CCITT, CommandClasses, EncapsulationFlags, SecurityClass, ZWaveError, ZWaveErrorCodes, randomBytes, securityClassIsS2, timespan, } from "@zwave-js/core";
import { containsCC } from "@zwave-js/serial/serialapi";
import { getEnumMemberName, throttle } from "@zwave-js/shared";
import { waitFor } from "@zwave-js/waddle";
import { distinct } from "alcalzone-shared/arrays";
import { wait } from "alcalzone-shared/async";
import { createDeferredPromise, } from "alcalzone-shared/deferred-promise";
import { roundTo } from "alcalzone-shared/math";
import { isArray } from "alcalzone-shared/typeguards";
import { TaskPriority, } from "../../driver/Task.js";
import { SchedulePollMixin } from "./60_ScheduledPoll.js";
/** Checks if a task belongs to a route rebuilding process */
export function isFirmwareUpdateOTATask(t) {
return t.tag?.id === "firmware-update-ota";
}
export class FirmwareUpdateMixin extends SchedulePollMixin {
async getFirmwareUpdateCapabilities() {
const api = this.commandClasses["Firmware Update Meta Data"];
const meta = await api.getMetaData();
if (!meta) {
throw new ZWaveError(`Failed to request firmware update capabilities: The node did not respond in time!`, ZWaveErrorCodes.Controller_NodeTimeout);
}
else if (!meta.firmwareUpgradable) {
return {
firmwareUpgradable: false,
};
}
return {
firmwareUpgradable: true,
// TODO: Targets are not the list of IDs - maybe expose the IDs as well?
firmwareTargets: Array
.from({ length: 1 + meta.additionalFirmwareIDs.length })
.fill(0)
.map((_, i) => i),
continuesToFunction: meta.continuesToFunction,
supportsActivation: meta.supportsActivation,
supportsResuming: meta.supportsResuming,
supportsNonSecureTransfer: meta.supportsNonSecureTransfer,
};
}
getFirmwareUpdateCapabilitiesCached() {
const firmwareUpgradable = this.getValue(FirmwareUpdateMetaDataCCValues.firmwareUpgradable.id);
const supportsActivation = this.getValue(FirmwareUpdateMetaDataCCValues.supportsActivation.id);
const continuesToFunction = this.getValue(FirmwareUpdateMetaDataCCValues.continuesToFunction.id);
const additionalFirmwareIDs = this.getValue(FirmwareUpdateMetaDataCCValues.additionalFirmwareIDs.id);
const supportsResuming = this.getValue(FirmwareUpdateMetaDataCCValues.supportsResuming.id);
const supportsNonSecureTransfer = this.getValue(FirmwareUpdateMetaDataCCValues.supportsNonSecureTransfer.id);
// Ensure all information was queried
if (!firmwareUpgradable
|| !isArray(additionalFirmwareIDs)) {
return { firmwareUpgradable: false };
}
return {
firmwareUpgradable: true,
// TODO: Targets are not the list of IDs - maybe expose the IDs as well?
firmwareTargets: Array
.from({ length: 1 + additionalFirmwareIDs.length })
.fill(0)
.map((_, i) => i),
continuesToFunction,
supportsActivation,
supportsResuming,
supportsNonSecureTransfer,
};
}
_abortFirmwareUpdate;
async abortFirmwareUpdate() {
if (!this._abortFirmwareUpdate)
return;
await this._abortFirmwareUpdate();
}
// Stores the CRC of the previously transferred firmware image.
// Allows detecting whether resuming is supported and where to continue in a multi-file transfer.
_previousFirmwareCRC;
/** Is used to remember fragment requests that came in before they were able to be handled */
_firmwareUpdatePrematureRequest;
async updateFirmware(updates, options = {}) {
if (updates.length === 0) {
throw new ZWaveError(`At least one update must be provided`, ZWaveErrorCodes.Argument_Invalid);
}
// Check that each update has a buffer with at least 1 byte
if (updates.some((u) => u.data.length === 0)) {
throw new ZWaveError(`All firmware updates must have a non-empty data buffer`, ZWaveErrorCodes.Argument_Invalid);
}
// Check that the targets are not duplicates
if (distinct(updates.map((u) => u.firmwareTarget ?? 0)).length
!== updates.length) {
throw new ZWaveError(`The target of all provided firmware updates must be unique`, ZWaveErrorCodes.Argument_Invalid);
}
// Don't start the process twice
if (this.driver.isOTWFirmwareUpdateInProgress()) {
throw new ZWaveError(`Failed to start the update: An OTW upgrade of the controller is in progress!`, ZWaveErrorCodes.FirmwareUpdateCC_Busy);
}
// Don't allow starting two firmware updates for the same node
const task = this.getUpdateFirmwareTask(updates, options);
if (task instanceof Promise) {
throw new ZWaveError(`Failed to start the update: A firmware update is already in progress for this node!`, ZWaveErrorCodes.FirmwareUpdateCC_Busy);
}
// Queue the task
return this.driver.scheduler.queueTask(task);
}
isFirmwareUpdateInProgress() {
return !!this.driver.scheduler.findTask(isFirmwareUpdateOTATask);
}
getUpdateFirmwareTask(updates, options = {}) {
const self = this;
// This task should only run once at a time
const existingTask = this.driver.scheduler.findTask((t) => t.tag?.id === "firmware-update-ota"
&& t.tag.nodeId === self.id);
if (existingTask)
return existingTask;
let keepAwake;
return {
// Firmware updates cause a lot of traffic. Execute them in the background.
priority: TaskPriority.Lower,
tag: { id: "firmware-update-ota", nodeId: self.id },
task: async function* firmwareUpdateTask() {
// Keep battery powered nodes awake during the process
keepAwake = self.keepAwake;
self.keepAwake = true;
// Support aborting the update
const abortContext = {
abort: false,
tooLateToAbort: false,
abortPromise: createDeferredPromise(),
};
self._abortFirmwareUpdate = async () => {
if (abortContext.tooLateToAbort) {
throw new ZWaveError(`The firmware update was transmitted completely, cannot abort anymore.`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToAbort);
}
self.driver.controllerLog.logNode(self.id, {
message: `Aborting firmware update...`,
direction: "outbound",
});
// Trigger the abort
abortContext.abort = true;
const aborted = await abortContext.abortPromise;
if (!aborted) {
throw new ZWaveError(`The node did not acknowledge the aborted update`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToAbort);
}
self.driver.controllerLog.logNode(self.id, {
message: `Firmware update aborted`,
direction: "inbound",
});
};
// Prepare the firmware update
let fragmentSizeSecure;
let fragmentSizeNonSecure;
let hardwareVersion;
let meta;
try {
const prepareResult = await self
.prepareFirmwareUpdateInternal(updates.map((u) => u.firmwareTarget ?? 0), abortContext);
// Handle early aborts
if (abortContext.abort) {
const result = {
success: false,
status: FirmwareUpdateStatus
.Error_TransmissionFailed,
reInterview: false,
};
self._emit("firmware update finished", self, result);
return result;
}
// If the firmware update was not aborted, prepareResult is definitely defined
({
fragmentSizeSecure,
fragmentSizeNonSecure,
hardwareVersion,
...meta
} = prepareResult);
}
catch {
// Not sure what the error is, but we'll label it "transmission failed"
const result = {
success: false,
status: FirmwareUpdateStatus.Error_TransmissionFailed,
reInterview: false,
};
return result;
}
yield; // Give the task scheduler time to do something else
// The resume and non-secure transfer features may not be supported by the node
// If not, disable them, even though the application requested them
if (!meta.supportsResuming)
options.resume = false;
const securityClass = self.getHighestSecurityClass();
const isSecure = securityClass === SecurityClass.S0_Legacy
|| securityClassIsS2(securityClass);
if (!isSecure) {
// The nonSecureTransfer option is only relevant for secure devices
options.nonSecureTransfer = false;
}
else if (!meta.supportsNonSecureTransfer) {
options.nonSecureTransfer = false;
}
// Throttle the progress emitter so applications can handle the load of events
const notifyProgress = throttle((progress) => self._emit("firmware update progress", self, progress), 250, true);
// If resuming is supported and desired, try to figure out with which file to continue
const updatesWithChecksum = updates.map((u) => ({
...u,
checksum: CRC16_CCITT(u.data),
}));
let skipFinishedFiles = -1;
let shouldResume = options.resume
&& self._previousFirmwareCRC != undefined;
if (shouldResume) {
skipFinishedFiles = updatesWithChecksum.findIndex((u) => u.checksum === self._previousFirmwareCRC);
if (skipFinishedFiles === -1)
shouldResume = false;
}
// Perform all firmware updates in sequence
let updateResult;
let conservativeWaitTime;
const totalBytes = updatesWithChecksum.reduce((total, update) => total + update.data.length, 0);
let sentBytesOfPreviousFiles = 0;
for (let i = 0; i < updatesWithChecksum.length; i++) {
const {
// If the firmware target is not given, update the Z-Wave chip
firmwareTarget: target = 0,
// If the firmware ID is not given, use the current one for the given chip
firmwareId = target === 0
? meta.firmwareId
: meta.additionalFirmwareIDs[target - 1], data, checksum, } = updatesWithChecksum[i];
if (i < skipFinishedFiles) {
// If we are resuming, skip this file since it was already done before
self.driver.controllerLog.logNode(self.id, `Skipping already completed firmware update (part ${i + 1} / ${updatesWithChecksum.length})...`);
sentBytesOfPreviousFiles += data.length;
continue;
}
self.driver.controllerLog.logNode(self.id, `Updating firmware (part ${i + 1} / ${updatesWithChecksum.length})...`);
// For determining the initial fragment size, assume the node respects our choice.
// If the node is not secure, these two values are identical anyways.
let fragmentSize = options.nonSecureTransfer
? fragmentSizeNonSecure
: fragmentSizeSecure;
// Tell the node to start requesting fragments
const { resume, nonSecureTransfer } = yield* self
.beginFirmwareUpdateInternal(data, meta.manufacturerId, target, firmwareId, fragmentSize, checksum, hardwareVersion, shouldResume, options.nonSecureTransfer);
// If the node did not accept non-secure transfer, revisit our choice of fragment size
if (options.nonSecureTransfer && !nonSecureTransfer) {
fragmentSize = fragmentSizeSecure;
}
// Remember the checksum, so we can resume if necessary
self._previousFirmwareCRC = checksum;
if (shouldResume) {
self.driver.controllerLog.logNode(self.id, `Node ${resume ? "accepted" : "did not accept"} resuming the update...`);
}
if (nonSecureTransfer) {
self.driver.controllerLog.logNode(self.id, `Firmware will be transferred without encryption...`);
}
yield; // Give the task scheduler time to do something else
// Listen for firmware update fragment requests and handle them
updateResult = yield* self.doFirmwareUpdateInternal(data, fragmentSize, nonSecureTransfer, abortContext, (fragment, total) => {
const progress = {
currentFile: i + 1,
totalFiles: updatesWithChecksum.length,
sentFragments: fragment,
totalFragments: total,
progress: roundTo(((sentBytesOfPreviousFiles
+ Math.min(fragment * fragmentSize, data.length))
/ totalBytes) * 100, 2),
};
notifyProgress(progress);
// When this file is done, add the fragments to the total, so we can compute the total progress correctly
if (fragment === total) {
sentBytesOfPreviousFiles += data.length;
}
});
// If we wait, wait a bit longer than the device told us, so it is actually ready to use
conservativeWaitTime = self.driver
.getConservativeWaitTimeAfterFirmwareUpdate(updateResult.waitTime);
if (!updateResult.success) {
self.driver.controllerLog.logNode(self.id, {
message: `Firmware update (part ${i + 1} / ${updatesWithChecksum.length}) failed with status ${getEnumMemberName(FirmwareUpdateStatus, updateResult.status)}`,
direction: "inbound",
});
const result = {
...updateResult,
waitTime: undefined,
reInterview: false,
};
self._emit("firmware update finished", self, result);
return result;
}
else if (i < updatesWithChecksum.length - 1) {
// Update succeeded, but we're not done yet
self.driver.controllerLog.logNode(self.id, {
message: `Firmware update (part ${i + 1} / ${updatesWithChecksum.length}) succeeded with status ${getEnumMemberName(FirmwareUpdateStatus, updateResult.status)}`,
direction: "inbound",
});
self.driver.controllerLog.logNode(self.id, `Continuing with next part in ${conservativeWaitTime} seconds...`);
// If we've resumed the previous file, there's no need to resume the next one too
shouldResume = false;
yield* waitFor(wait(conservativeWaitTime * 1000, true));
}
}
// We're done. No need to resume this update
self._previousFirmwareCRC = undefined;
const result = {
...updateResult,
waitTime: conservativeWaitTime,
reInterview: true,
};
// After a successful firmware update, we want to interview sleeping nodes immediately,
// so don't send them to sleep when they wake up
keepAwake = true;
self._emit("firmware update finished", self, result);
return result;
},
cleanup() {
self._abortFirmwareUpdate = undefined;
self._firmwareUpdatePrematureRequest = undefined;
// Make sure that the keepAwake flag gets reset at the end
self.keepAwake = keepAwake;
if (!keepAwake) {
setImmediate(() => {
self.driver.debounceSendNodeToSleep(self);
});
}
return Promise.resolve();
},
};
}
/** Prepares the firmware update of a single target by collecting the necessary information */
async prepareFirmwareUpdateInternal(targets, abortContext) {
const api = this.commandClasses["Firmware Update Meta Data"];
// ================================
// STEP 1:
// Check if this update is possible
const meta = await api.getMetaData();
if (!meta) {
throw new ZWaveError(`Failed to start the update: The node did not respond in time!`, ZWaveErrorCodes.Controller_NodeTimeout);
}
for (const target of targets) {
if (target === 0) {
if (!meta.firmwareUpgradable) {
throw new ZWaveError(`Failed to start the update: The Z-Wave chip firmware is not upgradable!`, ZWaveErrorCodes.FirmwareUpdateCC_NotUpgradable);
}
}
else {
if (api.version < 3) {
throw new ZWaveError(`Failed to start the update: The node does not support upgrading a different firmware target than 0!`, ZWaveErrorCodes.FirmwareUpdateCC_TargetNotFound);
}
else if (meta.additionalFirmwareIDs[target - 1] == undefined) {
throw new ZWaveError(`Failed to start the update: Firmware target #${target} not found on this node!`, ZWaveErrorCodes.FirmwareUpdateCC_TargetNotFound);
}
}
}
// ================================
// STEP 2:
// Determine the fragment size
const fcc = new FirmwareUpdateMetaDataCC({ nodeId: this.id });
fcc.toggleEncapsulationFlag(EncapsulationFlags.Security, this.driver.isCCSecure(fcc.ccId, this.id));
const maxGrossPayloadSizeSecure = this.driver
.computeNetCCPayloadSize(fcc);
const maxGrossPayloadSizeNonSecure = this.driver
.computeNetCCPayloadSize(fcc, true);
const ccVersion = getEffectiveCCVersion(this.driver, fcc);
const maxNetPayloadSizeSecure = maxGrossPayloadSizeSecure
- 2 // report number
- (ccVersion >= 2 ? 2 : 0); // checksum
const maxNetPayloadSizeNonSecure = maxGrossPayloadSizeNonSecure
- 2 // report number
- (ccVersion >= 2 ? 2 : 0); // checksum
// Use the smallest allowed payload
const fragmentSizeSecure = Math.min(maxNetPayloadSizeSecure, meta.maxFragmentSize ?? Number.POSITIVE_INFINITY);
const fragmentSizeNonSecure = Math.min(maxNetPayloadSizeNonSecure, meta.maxFragmentSize ?? Number.POSITIVE_INFINITY);
if (abortContext.abort) {
abortContext.abortPromise.resolve(true);
return;
}
else {
return {
...meta,
fragmentSizeSecure,
fragmentSizeNonSecure,
};
}
}
async handleUnexpectedFirmwareUpdateGet(command) {
// This method will only be called under two circumstances:
// 1. The node is currently busy responding to a firmware update request -> remember the request
if (this.isFirmwareUpdateInProgress()) {
this._firmwareUpdatePrematureRequest = command;
return;
}
// 2. No firmware update is in progress -> abort
this.driver.controllerLog.logNode(this.id, {
message: `Received Firmware Update Get, but no firmware update is in progress. Forcing the node to abort...`,
direction: "inbound",
});
// Since no update is in progress, we need to determine the fragment size again
const fcc = new FirmwareUpdateMetaDataCC({ nodeId: this.id });
fcc.toggleEncapsulationFlag(EncapsulationFlags.Security, !!(command.encapsulationFlags & EncapsulationFlags.Security));
const ccVersion = getEffectiveCCVersion(this.driver, fcc);
const fragmentSize = this.driver.computeNetCCPayloadSize(fcc)
- 2 // report number
- (ccVersion >= 2 ? 2 : 0); // checksum
const fragment = randomBytes(fragmentSize);
try {
await this.sendCorruptedFirmwareUpdateReport(command.reportNumber, fragment);
}
catch {
// ignore
}
}
/** Kicks off a firmware update of a single target. Returns whether the node accepted resuming and non-secure transfer */
async *beginFirmwareUpdateInternal(data, manufacturerId, target, firmwareId, fragmentSize, checksum, hardwareVersion, resume, nonSecureTransfer) {
const api = this.commandClasses["Firmware Update Meta Data"];
// ================================
// STEP 3:
// Start the update
this.driver.controllerLog.logNode(this.id, {
message: `Starting firmware update...`,
direction: "outbound",
});
// Request the node to start the upgrade
let result = await api.requestUpdate({
// TODO: Should manufacturer id be provided externally?
manufacturerId,
firmwareId,
firmwareTarget: target,
fragmentSize,
checksum,
hardwareVersion,
resume,
nonSecureTransfer,
});
// On some devices the response can take a minute or so to be received,
// probably because of a manual out-of-band activation.
if (!result) {
result = yield* waitFor(this.driver
.waitForCommand((cc) => cc instanceof FirmwareUpdateMetaDataCCRequestReport
&& cc.nodeId === this.id, 60000));
}
switch (result.status) {
case FirmwareUpdateRequestStatus.Error_AuthenticationExpected:
throw new ZWaveError(`Failed to start the update: A manual authentication event (e.g. button push) was expected!`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToStart);
case FirmwareUpdateRequestStatus.Error_BatteryLow:
throw new ZWaveError(`Failed to start the update: The battery level is too low!`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToStart);
case FirmwareUpdateRequestStatus
.Error_FirmwareUpgradeInProgress:
throw new ZWaveError(`Failed to start the update: A firmware upgrade is already in progress!`, ZWaveErrorCodes.FirmwareUpdateCC_Busy);
case FirmwareUpdateRequestStatus
.Error_InvalidManufacturerOrFirmwareID:
throw new ZWaveError(`Failed to start the update: Invalid manufacturer or firmware id!`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToStart);
case FirmwareUpdateRequestStatus.Error_InvalidHardwareVersion:
throw new ZWaveError(`Failed to start the update: Invalid hardware version!`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToStart);
case FirmwareUpdateRequestStatus.Error_NotUpgradable:
throw new ZWaveError(`Failed to start the update: Firmware target #${target} is not upgradable!`, ZWaveErrorCodes.FirmwareUpdateCC_NotUpgradable);
case FirmwareUpdateRequestStatus.Error_FragmentSizeTooLarge:
throw new ZWaveError(`Failed to start the update: The chosen fragment size is too large!`, ZWaveErrorCodes.FirmwareUpdateCC_FailedToStart);
case FirmwareUpdateRequestStatus.OK:
// All good, we have started!
// Keep the node awake until the update is done.
this.keepAwake = true;
}
return {
resume: !!result.resume,
nonSecureTransfer: !!result.nonSecureTransfer,
};
}
async handleFirmwareUpdateMetaDataGet(command) {
const endpoint = this.getEndpoint(command.endpointIndex)
?? this;
// We are being queried, so the device may actually not support the CC, just control it.
// Using the commandClasses property would throw in that case
const api = endpoint
.createAPI(CommandClasses["Firmware Update Meta Data"], false)
.withOptions({
// Answer with the same encapsulation as asked, but omit
// Supervision as it shouldn't be used for Get-Report flows
encapsulationFlags: command.encapsulationFlags
& ~EncapsulationFlags.Supervision,
});
// We do not support the firmware to be upgraded.
await api.reportMetaData({
manufacturerId: this.driver.options.vendor?.manufacturerId
?? 0xffff,
firmwareUpgradable: false,
hardwareVersion: this.driver.options.vendor?.hardwareVersion
?? 0,
// We must advertise Z-Wave JS itself as firmware 1
// No firmware is upgradable, so we advertise firmware id 0
additionalFirmwareIDs: [0],
});
}
async handleFirmwareUpdateRequestGet(command) {
const endpoint = this.getEndpoint(command.endpointIndex)
?? this;
// We are being queried, so the device may actually not support the CC, just control it.
// Using the commandClasses property would throw in that case
const api = endpoint
.createAPI(CommandClasses["Firmware Update Meta Data"], false)
.withOptions({
// Answer with the same encapsulation as asked, but omit
// Supervision as it shouldn't be used for Get-Report flows
encapsulationFlags: command.encapsulationFlags
& ~EncapsulationFlags.Supervision,
});
// We do not support the firmware to be upgraded.
await api.respondToUpdateRequest({
status: FirmwareUpdateRequestStatus.Error_NotUpgradable,
});
}
async handleFirmwareUpdatePrepareGet(command) {
const endpoint = this.getEndpoint(command.endpointIndex)
?? this;
// We are being queried, so the device may actually not support the CC, just control it.
// Using the commandClasses property would throw in that case
const api = endpoint
.createAPI(CommandClasses["Firmware Update Meta Data"], false)
.withOptions({
// Answer with the same encapsulation as asked, but omit
// Supervision as it shouldn't be used for Get-Report flows
encapsulationFlags: command.encapsulationFlags
& ~EncapsulationFlags.Supervision,
});
// We do not support the firmware to be downloaded
await api.respondToDownloadRequest({
status: FirmwareDownloadStatus.Error_NotDownloadable,
checksum: 0x0000,
});
}
async sendCorruptedFirmwareUpdateReport(reportNum, fragment, nonSecureTransfer = false) {
try {
await this.commandClasses["Firmware Update Meta Data"]
.withOptions({
// Only encapsulate if the transfer is secure
autoEncapsulate: !nonSecureTransfer,
})
.sendFirmwareFragment(reportNum, true, fragment);
}
catch {
// ignore
}
}
hasPendingFirmwareUpdateFragment(fragmentNumber) {
// Avoid queuing duplicate fragments
const isCurrentFirmwareFragment = (t) => t.message.getNodeId() === this.id
&& containsCC(t.message)
&& t.message.command instanceof FirmwareUpdateMetaDataCCReport
&& t.message.command.reportNumber === fragmentNumber;
return this.driver.hasPendingTransactions(isCurrentFirmwareFragment);
}
async *doFirmwareUpdateInternal(data, fragmentSize, nonSecureTransfer, abortContext, onProgress) {
const numFragments = Math.ceil(data.length / fragmentSize);
// Make sure we're not responding to an outdated request immediately
this._firmwareUpdatePrematureRequest = undefined;
// ================================
// STEP 4:
// Respond to fragment requests from the node
update: while (true) {
yield; // Give the task scheduler time to do something else
// During ongoing firmware updates, it can happen that the next request is received before the callback for the previous response
// is back. In that case we can immediately handle the premature request. Otherwise wait for the next request.
let fragmentRequest;
if (this._firmwareUpdatePrematureRequest) {
fragmentRequest = this._firmwareUpdatePrematureRequest;
this._firmwareUpdatePrematureRequest = undefined;
}
else {
try {
fragmentRequest = yield* waitFor(this.driver
.waitForCommand((cc) => cc.nodeId === this.id
&& cc
instanceof FirmwareUpdateMetaDataCCGet,
// Wait up to 2 minutes for each fragment request.
// Some users try to update devices with unstable connections, where 30s can be too short.
timespan.minutes(2)));
}
catch {
// In some cases it can happen that the device stops requesting update frames
// We need to timeout the update in this case so it can be restarted
this.driver.controllerLog.logNode(this.id, {
message: `Firmware update timed out`,
direction: "none",
level: "warn",
});
return {
success: false,
status: FirmwareUpdateStatus.Error_Timeout,
};
}
}
// When a node requests a firmware update fragment, it must be awake
this.markAsAwake();
if (fragmentRequest.reportNumber > numFragments) {
this.driver.controllerLog.logNode(this.id, {
message: `Received Firmware Update Get for an out-of-bounds fragment. Forcing the node to abort...`,
direction: "inbound",
});
await this.sendCorruptedFirmwareUpdateReport(fragmentRequest.reportNumber, randomBytes(fragmentSize), nonSecureTransfer);
// This will cause the node to abort the process, wait for that
break update;
}
// Actually send the requested frames
request: for (let num = fragmentRequest.reportNumber; num
< fragmentRequest.reportNumber
+ fragmentRequest.numReports; num++) {
yield; // Give the task scheduler time to do something else
// Check if the node requested more fragments than are left
if (num > numFragments) {
break;
}
const fragment = data.subarray((num - 1) * fragmentSize, num * fragmentSize);
if (abortContext.abort) {
await this.sendCorruptedFirmwareUpdateReport(fragmentRequest.reportNumber, randomBytes(fragment.length), nonSecureTransfer);
// This will cause the node to abort the process, wait for that
break update;
}
else {
// Avoid queuing duplicate fragments
if (this.hasPendingFirmwareUpdateFragment(num)) {
this.driver.controllerLog.logNode(this.id, {
message: `Firmware fragment ${num} already queued`,
level: "warn",
});
continue request;
}
this.driver.controllerLog.logNode(this.id, {
message: `Sending firmware fragment ${num} / ${numFragments}`,
direction: "outbound",
});
const isLast = num === numFragments;
try {
await this
.commandClasses["Firmware Update Meta Data"]
.withOptions({
// Only encapsulate if the transfer is secure
autoEncapsulate: !nonSecureTransfer,
})
.sendFirmwareFragment(num, isLast, fragment);
onProgress(num, numFragments);
// If that was the last one wait for status report from the node and restart interview
if (isLast) {
abortContext.tooLateToAbort = true;
break update;
}
}
catch {
// When transmitting fails, simply stop responding to this request and wait for the node to re-request the fragment
this.driver.controllerLog.logNode(this.id, {
message: `Failed to send firmware fragment ${num} / ${numFragments}`,
direction: "outbound",
level: "warn",
});
break request;
}
}
}
}
yield; // Give the task scheduler time to do something else
// ================================
// STEP 5:
// Finalize the update process
const statusReport = yield* waitFor(this.driver
.waitForCommand((cc) => cc.nodeId === this.id
&& cc
instanceof FirmwareUpdateMetaDataCCStatusReport,
// Wait up to 5 minutes. It should never take that long, but the specs
// don't say anything specific
5 * 60000)
.catch(() => undefined));
if (abortContext.abort) {
abortContext.abortPromise.resolve(statusReport?.status
=== FirmwareUpdateStatus.Error_TransmissionFailed);
}
if (!statusReport) {
this.driver.controllerLog.logNode(this.id, `The node did not acknowledge the completed update`, "warn");
return {
success: false,
status: FirmwareUpdateStatus.Error_Timeout,
};
}
const { status, waitTime } = statusReport;
// Actually, OK_WaitingForActivation should never happen since we don't allow
// delayed activation in the RequestGet command
const success = status >= FirmwareUpdateStatus.OK_WaitingForActivation;
return {
success,
status,
waitTime,
};
}
}
//# sourceMappingURL=70_FirmwareUpdate.js.map