motion-master-client 0.0.422 → 0.0.423

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.
@@ -310,47 +310,61 @@ class MotionMasterReqResClient {
310
310
  *
311
311
  * If the requested file content is empty, Motion Master will return a not found error. This is subject to change now that Motion Master reads the file error codes.
312
312
  *
313
- * SOMANET_CiA_402.xml and SOMANET_CiA_402.xml.zip files are stored in IndexedDB for faster access.
313
+ * The ESI file ("SOMANET_CiA_402.xml" or "SOMANET_CiA_402.xml.zip") is never read from the device, see {@link getEsiFromIndexedDB}.
314
314
  */
315
315
  getDeviceFile(props, requestTimeout, messageId) {
316
316
  return this.resolveDeviceAddress(props).pipe((0, operators_1.mergeMap)((deviceAddress) => {
317
- if ((props.name === 'SOMANET_CiA_402.xml' || props.name === 'SOMANET_CiA_402.xml.zip') &&
318
- // @ts-ignore
319
- typeof window !== 'undefined' &&
320
- // @ts-ignore
321
- window.indexedDB) {
322
- return this.getParameterValue(deviceAddress, 0x100a, 0x00).pipe((0, operators_1.mergeMap)((firmwareVersion) => {
323
- return (0, rxjs_1.from)(this.mmcidb.open()).pipe((0, operators_1.mergeMap)(() => this.mmcidb.getEsi(firmwareVersion)), (0, operators_1.mergeMap)((data) => {
324
- if (data) {
325
- const status = {
326
- request: 'succeeded',
327
- name: props.name,
328
- deviceAddress,
329
- content: data,
330
- success: {
331
- code: types_1.MotionMasterMessage.Status.DeviceFile.Success.Code.UNSPECIFIED,
332
- message: 'ESI file successfully retrieved from IndexedDB.',
333
- },
334
- messageId,
335
- };
336
- return (0, rxjs_1.of)(status);
337
- }
338
- else {
339
- return this.getDeviceFileUsingResolvedDeviceAddress(deviceAddress, props, requestTimeout, messageId).pipe((0, rxjs_1.tap)((status) => {
340
- if (status.content) {
341
- this.mmcidb.putEsi(firmwareVersion, new Uint8Array(status.content));
342
- }
343
- }));
344
- }
345
- }));
346
- }), (0, operators_1.catchError)((err) => {
347
- console.error('Failed to retrieve and cache ESI file from IndexedDB:', err);
348
- return this.getDeviceFileUsingResolvedDeviceAddress(deviceAddress, props, requestTimeout, messageId);
349
- }));
317
+ if (props.name === firmware_esi_1.esiFilename || props.name === firmware_esi_1.esiZipFilename) {
318
+ return this.getEsiFromIndexedDB(deviceAddress, props.name, messageId);
350
319
  }
351
- else {
352
- return this.getDeviceFileUsingResolvedDeviceAddress(deviceAddress, props, requestTimeout, messageId);
320
+ return this.getDeviceFileUsingResolvedDeviceAddress(deviceAddress, props, requestTimeout, messageId);
321
+ }));
322
+ }
323
+ /**
324
+ * Returns the ESI file cached in IndexedDB under the firmware version the device reports in 0x100A:00.
325
+ *
326
+ * The ESI file is never read from the device. Firmware installation no longer writes it to the device (see
327
+ * {@link installFirmwareAndCacheEsi}), and Motion Master does not remove a skipped file, so the file on the device may
328
+ * belong to an older firmware. Neither the file nor its ESI revision tells which firmware it belongs to.
329
+ * Caching it under the installed version would hide the correct ESI file for good.
330
+ *
331
+ * The ESI file gets into IndexedDB only from a firmware package: at firmware installation, see
332
+ * {@link cacheEsiFromFirmwarePackage}, or when a user loads it from a package in OBLAC Drives.
333
+ *
334
+ * Returns a failed status with the NOT_FOUND error code when the ESI file is not cached for the installed firmware,
335
+ * or when there is no IndexedDB, as outside the browser. Returns a failed status with the FAILED error code when the
336
+ * firmware version or IndexedDB can't be read.
337
+ */
338
+ getEsiFromIndexedDB(deviceAddress, name, messageId) {
339
+ const failed = (code, message) => ({
340
+ request: 'failed',
341
+ name,
342
+ deviceAddress,
343
+ error: { code, message },
344
+ messageId,
345
+ });
346
+ if (!globalThis.indexedDB) {
347
+ return (0, rxjs_1.of)(failed(types_1.MotionMasterMessage.Status.DeviceFile.Error.Code.NOT_FOUND, 'The ESI file is not read from the device, and there is no IndexedDB to read it from.'));
348
+ }
349
+ return this.getParameterValue(deviceAddress, 0x100a, 0x00).pipe((0, operators_1.mergeMap)((firmwareVersion) => (0, rxjs_1.from)(this.mmcidb.open().then(() => this.mmcidb.getEsi(firmwareVersion))).pipe((0, operators_1.map)((data) => {
350
+ if (data) {
351
+ return {
352
+ request: 'succeeded',
353
+ name,
354
+ deviceAddress,
355
+ content: data,
356
+ success: {
357
+ code: types_1.MotionMasterMessage.Status.DeviceFile.Success.Code.UNSPECIFIED,
358
+ message: 'ESI file successfully retrieved from IndexedDB.',
359
+ },
360
+ messageId,
361
+ };
353
362
  }
363
+ return failed(types_1.MotionMasterMessage.Status.DeviceFile.Error.Code.NOT_FOUND, `The ESI file for firmware ${firmwareVersion} is not cached. The ESI file is not read from the device.`);
364
+ }))), (0, operators_1.catchError)((err) => {
365
+ console.error('Failed to retrieve the ESI file from IndexedDB:', err);
366
+ const reason = err instanceof Error ? err.message : String(err);
367
+ return (0, rxjs_1.of)(failed(types_1.MotionMasterMessage.Status.DeviceFile.Error.Code.FAILED, `Failed to retrieve the ESI file from IndexedDB: ${reason}`));
354
368
  }));
355
369
  }
356
370
  getDeviceFileUsingResolvedDeviceAddress(deviceAddress, props, requestTimeout, messageId) {
@@ -507,8 +521,7 @@ class MotionMasterReqResClient {
507
521
  * Extracts the ESI file (SOMANET_CiA_402.xml) from a firmware package and stores its unzipped content in
508
522
  * IndexedDB, keyed by the given firmware version.
509
523
  *
510
- * This is the install-time counterpart to the read-time caching in {@link getDeviceFile}: once the ESI is
511
- * cached here, OBLAC can resolve it from IndexedDB without reading it back from the drive. This is what
524
+ * IndexedDB is the only source of the ESI file: {@link getDeviceFile} never reads it from the drive. This is what
512
525
  * allows the ESI file (SOMANET_CiA_402.xml.zip) to be skipped during firmware installation, which avoids
513
526
  * slow and unreliable FoE transfers and works around devices (e.g. NetX) that reject writing that file.
514
527
  *
@@ -542,17 +555,18 @@ class MotionMasterReqResClient {
542
555
  * user-overridable: when `props.skipFiles` is provided it is passed through unchanged, so a caller can choose to
543
556
  * still write the ESI by omitting it from that list.
544
557
  * - Extracts the ESI from the package and stores it in IndexedDB keyed by firmware version (see
545
- * {@link cacheEsiFromFirmwarePackage}) *before* the installation starts, so it can be resolved locally by
546
- * {@link getDeviceFile}. It must be cached up front because, with the ESI skipped on the drive, Motion Master
547
- * will try to read it back right after installation. This caching happens regardless of whether the ESI is
548
- * also written to the drive. A caching failure is logged but does not prevent installation.
558
+ * {@link cacheEsiFromFirmwarePackage}) *before* the installation starts, so {@link getDeviceFile} can return it
559
+ * as soon as the installation ends. IndexedDB is the only source of the ESI file: {@link getDeviceFile} never
560
+ * reads it from the drive, even when the ESI is also written there. A caching failure is logged but does not
561
+ * prevent installation; the ESI file is then not found until a user loads it from the package (Load ESI in
562
+ * OBLAC Drives).
549
563
  *
550
564
  * @param firmwareVersion - The firmware version to key the cached ESI by; this must match the value reported by
551
565
  * the device parameter 0x100a:00 after installation, which {@link getDeviceFile} uses.
552
566
  * For OBLAC firmware packages this is the version encoded in the package filename, see
553
567
  * {@link extractFirmwareVersionFromFirmwarePackageFilename}. When it cannot be determined
554
568
  * (e.g. an arbitrary package with an unrecognized filename), pass an empty value to skip
555
- * caching; the read path then falls back to the online source.
569
+ * caching; the ESI file is then not found unless it was cached before.
556
570
  */
557
571
  installFirmwareAndCacheEsi(firmwareVersion, props, requestTimeout, messageId) {
558
572
  var _a;
@@ -3610,45 +3624,94 @@ class MotionMasterReqResClient {
3610
3624
  * The commands execute only when subscribed to, ensuring a strict sequence.
3611
3625
  */
3612
3626
  updateSmmSoftware(deviceRef, username, password, buffer, crc, chunkSize = 1000, commandTimeout = 30000, responsePollingInterval = 1000, fsBufferReadWriteTimeout = 120000) {
3627
+ return this.updateSmmSoftwareWithProgress(deviceRef, username, password, buffer, crc, chunkSize, commandTimeout, responsePollingInterval, fsBufferReadWriteTimeout).pipe((0, operators_1.last)(), (0, operators_1.map)(() => undefined));
3628
+ }
3629
+ /**
3630
+ * Performs a complete SMM software update like {@link updateSmmSoftware}, reporting progress as it goes.
3631
+ *
3632
+ * A {@link SmmSoftwareUpdateProgress} value is emitted when each stage starts, once per transmitted chunk,
3633
+ * and a final `done` value after the SMM restart has been triggered. Use it to show the user what the
3634
+ * update is doing; transmitting a large software file can take many minutes.
3635
+ *
3636
+ * Parameters are the same as for {@link updateSmmSoftware}.
3637
+ *
3638
+ * @throws {Error} If `buffer` is empty or `chunkSize` is not a positive integer. Nothing is sent to the device then.
3639
+ * @throws {Error} If any step in the software update process fails.
3640
+ */
3641
+ updateSmmSoftwareWithProgress(deviceRef, username, password, buffer, crc, chunkSize = 1000, commandTimeout = 30000, responsePollingInterval = 1000, fsBufferReadWriteTimeout = 120000) {
3613
3642
  const fileSize = buffer.byteLength;
3614
- // Build the sequence of observables that perform each step of the update process.
3643
+ // A NaN chunk size (e.g. from a disabled form control) would otherwise skip every chunk
3644
+ // and finalize an update with no software transmitted.
3645
+ if (!Number.isInteger(chunkSize) || chunkSize <= 0) {
3646
+ return (0, rxjs_1.throwError)(() => new Error(`Invalid SMM software update chunk size: ${chunkSize}. It must be a positive integer.`));
3647
+ }
3648
+ if (fileSize === 0) {
3649
+ return (0, rxjs_1.throwError)(() => new Error('The SMM software file is empty.'));
3650
+ }
3651
+ const chunks = Math.ceil(fileSize / chunkSize); // Total number of chunks to send.
3652
+ // Reports the stage, then runs its work. Deferred, so each step starts only after the previous one completes.
3653
+ const step = (stage, work, chunk = 0) => (0, rxjs_1.concat)((0, rxjs_1.of)({ stage, chunk, chunks }), (0, rxjs_1.defer)(work).pipe((0, operators_1.ignoreElements)()));
3615
3654
  const observables = [
3616
3655
  // Step 1: Ensure any existing session is logged out before starting the update.
3617
- (0, rxjs_1.defer)(() => this.logoutFromSmm(deviceRef)),
3656
+ step('logout', () => this.logoutFromSmm(deviceRef)),
3618
3657
  // Step 2: Log in to the device specifically for software update permissions.
3619
- (0, rxjs_1.defer)(() => this.loginToSmmForSoftwareUpdate(deviceRef, username, password)),
3658
+ step('login', () => this.loginToSmmForSoftwareUpdate(deviceRef, username, password)),
3620
3659
  // Step 3: Inform the device about the incoming file size for proper setup.
3621
- (0, rxjs_1.defer)(() => this.configureSmmSoftwareUpdate(deviceRef, fileSize)),
3660
+ step('configure', () => this.configureSmmSoftwareUpdate(deviceRef, fileSize)),
3622
3661
  ];
3623
- const n = Math.ceil(fileSize / chunkSize); // Total number of chunks to send.
3624
3662
  // Step 4: Transmit the update file chunk-by-chunk.
3625
3663
  const transmitSoftwareCommand = (0, os_command_1.createSmmAcyclicHandlerOsCommand)(os_command_1.SmmAcyclicHandlerSubcommandId.TRANSMIT_SOFTWARE);
3626
- for (let i = 0; i < n; i++) {
3664
+ for (let i = 0; i < chunks; i++) {
3627
3665
  const start = i * chunkSize;
3628
- const end = start + chunkSize;
3629
- const fsBufferContent = buffer.slice(start, end);
3630
- observables.push((0, rxjs_1.defer)(() => this.runOsCommandAndWriteFsBuffer(deviceRef, transmitSoftwareCommand, fsBufferContent, commandTimeout, responsePollingInterval, false, fsBufferReadWriteTimeout).pipe((0, operators_1.last)(), // Only consider the final response of the command.
3666
+ const fsBufferContent = buffer.slice(start, start + chunkSize);
3667
+ observables.push(step('transmit', () => this.runOsCommandAndWriteFsBuffer(deviceRef, transmitSoftwareCommand, fsBufferContent, commandTimeout, responsePollingInterval, false, fsBufferReadWriteTimeout).pipe((0, operators_1.last)(), // Only consider the final response of the command.
3631
3668
  (0, operators_1.map)((response) => {
3632
3669
  if (response.request !== 'succeeded') {
3633
- throw new Error(`Transmitting SMM software chunk ${i + 1}/${n} failed: ${(0, os_command_1.describeOsCommandFailure)(response)}.`);
3670
+ throw new Error(`Transmitting SMM software chunk ${i + 1}/${chunks} failed: ${(0, os_command_1.describeOsCommandFailure)(response)}.`);
3634
3671
  }
3635
- }) // Interpret the response success.
3636
- )));
3672
+ })), i + 1));
3637
3673
  }
3638
- // Step 5: Pack the CRC value and prepare the finalize command.
3674
+ // Step 5: Send the CRC to validate data integrity and finalize the update.
3639
3675
  const packedCrc = (0, smm_1.packSmmSoftwareUpdateCrc)(crc);
3640
3676
  const finalizeSoftwareUpdateCommand = (0, os_command_1.createSmmAcyclicHandlerOsCommand)(os_command_1.SmmAcyclicHandlerSubcommandId.FINALIZE_SOFTWARE_UPDATE);
3641
- // Step 6: Send the CRC to validate data integrity.
3642
- observables.push((0, rxjs_1.defer)(() =>
3677
+ observables.push(step('finalize', () =>
3643
3678
  // @ts-ignore
3644
3679
  this.runOsCommandAndWriteFsBuffer(deviceRef, finalizeSoftwareUpdateCommand, packedCrc).pipe((0, operators_1.last)(), (0, operators_1.map)((response) => {
3645
3680
  if (response.request !== 'succeeded') {
3646
- throw new Error(`Finalizing SMM software update failed: ${(0, os_command_1.describeOsCommandFailure)(response)}.`);
3681
+ throw new Error(`Finalizing SMM software update failed: ${(0, os_command_1.describeOsCommandFailure)(response)}. Check that the CRC matches the software file.`);
3647
3682
  }
3648
- }))),
3649
- // Step 7: Trigger a restart so the new software takes effect.
3650
- (0, rxjs_1.defer)(() => this.triggerSmmRestart(deviceRef)));
3651
- return (0, rxjs_1.concat)(...observables).pipe((0, operators_1.last)());
3683
+ })), chunks),
3684
+ // Step 6: Trigger a restart so the new software takes effect.
3685
+ step('restart', () => this.triggerSmmRestart(deviceRef), chunks), (0, rxjs_1.of)({ stage: 'done', chunk: chunks, chunks }));
3686
+ return (0, rxjs_1.concat)(...observables);
3687
+ }
3688
+ /**
3689
+ * Waits until the SMM reports the expected software version.
3690
+ *
3691
+ * After an SMM software update the SMM restarts and boots the new software, which can take around 30 s.
3692
+ * Reads may fail or return the previous version during that time, so the version is polled until it
3693
+ * matches or the timeout expires.
3694
+ *
3695
+ * @param deviceRef - Reference to the target device.
3696
+ * @param expectedVersion - The expected SMM software version, bitwise encoded (e.g. 25344 for v99.0).
3697
+ * @param timeout - (Optional) How long to wait, in ms. Defaults to 90,000 ms.
3698
+ * @param pollingInterval - (Optional) Delay between reads, in ms. Defaults to 2000 ms.
3699
+ *
3700
+ * @throws {Error} If the SMM does not report the expected version within `timeout`.
3701
+ */
3702
+ waitForSmmFirmwareVersion(deviceRef, expectedVersion, timeout = 90000, pollingInterval = 2000) {
3703
+ let lastVersion;
3704
+ let lastError;
3705
+ const read$ = (0, rxjs_1.defer)(() => this.readSmmFirmwareVersionAsNumber(deviceRef)).pipe((0, rxjs_1.tap)((version) => (lastVersion = version)), (0, operators_1.catchError)((err) => {
3706
+ lastError = err;
3707
+ return rxjs_1.EMPTY;
3708
+ }));
3709
+ return read$.pipe((0, operators_1.repeat)({ delay: () => (lastVersion === expectedVersion ? rxjs_1.EMPTY : (0, rxjs_1.timer)(pollingInterval)) }), (0, operators_1.filter)((version) => version === expectedVersion), (0, operators_1.take)(1), (0, operators_1.map)(() => undefined), (0, operators_1.takeUntil)((0, rxjs_1.timer)(timeout).pipe((0, operators_1.mergeMap)(() => {
3710
+ const reported = lastVersion !== undefined
3711
+ ? `it reports v${(0, smm_1.bitwiseDecodeNumberVersion)(lastVersion)}`
3712
+ : `reading the version failed: ${lastError instanceof Error ? lastError.message : String(lastError)}`;
3713
+ return (0, rxjs_1.throwError)(() => new Error(`SMM did not report software v${(0, smm_1.bitwiseDecodeNumberVersion)(expectedVersion)} within ${timeout / 1000} s after the update; ${reported}.`));
3714
+ }))));
3652
3715
  }
3653
3716
  /**
3654
3717
  * Resolves whether an automated SMM software update is allowed for the specified device.
@@ -3660,7 +3723,8 @@ class MotionMasterReqResClient {
3660
3723
  *
3661
3724
  * The automation is only permitted when **all** of the following conditions are met:
3662
3725
  * - The device is a recognized Somanet product (`isValidProduct`).
3663
- * - The device belongs to the Circulo product family (`isCirculo`).
3726
+ * - The device belongs to the Circulo product family (`isCirculo`). ACTILINK-JD / JD Duo devices are judged
3727
+ * by the underlying Circulo from their hardware description, see {@link resolveEncoderProductId}.
3664
3728
  * - The binary is encrypted (`isBinaryEncrypted`).
3665
3729
  * - The binary targets one of the supported SMM versions — v1.4 (260) or v2.56 (568) (`isAllowedBinaryVersion`).
3666
3730
  * - If targeting v2.56, the device SoC firmware version is ≥ 5.5.0 (`canInstallV256`).
@@ -3675,7 +3739,7 @@ class MotionMasterReqResClient {
3675
3739
  *
3676
3740
  * @returns A promise that resolves to an {@link SmmAutomatedUpdate} object containing:
3677
3741
  * - `isValidProduct` — Whether the device maps to a known Somanet product.
3678
- * - `isCirculo` — Whether the device belongs to the Circulo product family.
3742
+ * - `isCirculo` — Whether the device is a Circulo or a Circulo-based ACTILINK-JD / JD Duo.
3679
3743
  * - `isCirculo7` — Whether the device is a Circulo 7.
3680
3744
  * - `isCirculo9` — Whether the device is a Circulo 9.
3681
3745
  * - `isDeviceLw1` — Whether the device SMM firmware is below v1.3 (version < 259).
@@ -3689,10 +3753,11 @@ class MotionMasterReqResClient {
3689
3753
  * - `binaryVersion` — The target SMM version encoded in the binary header.
3690
3754
  */
3691
3755
  resolveSmmAutomatedUpdateConditions(deviceRef, buffer) {
3692
- var _a, _b;
3756
+ var _a, _b, _c;
3693
3757
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
3694
3758
  const device = yield (0, rxjs_1.lastValueFrom)(this.resolveDevice(deviceRef));
3695
- const product = (0, product_1.findSomanetProductById)(device === null || device === void 0 ? void 0 : device.serialNumber);
3759
+ // ACTILINK-JD / JD Duo wrap a Circulo; judge the product by the underlying Circulo device.
3760
+ const product = (0, product_1.findSomanetProductById)((0, product_1.resolveEncoderProductId)((_a = device === null || device === void 0 ? void 0 : device.hardwareDescription) !== null && _a !== void 0 ? _a : device === null || device === void 0 ? void 0 : device.serialNumber));
3696
3761
  const isValidProduct = !!product;
3697
3762
  // v1.4 (no safe torque), v2.56 (safe torque)
3698
3763
  const allowedTargetSmmVersions = [260, 568];
@@ -3722,7 +3787,7 @@ class MotionMasterReqResClient {
3722
3787
  const binaryHeader = (0, smm_1.parseSmmBinaryHeaders)(buffer);
3723
3788
  const binaryInformation = (0, smm_1.getInformationFromSmmFirmwareVersion)(binaryHeader.swVersion);
3724
3789
  // Allow installation of smm v2.56 only if device SoC firmware version is >=5.5.0
3725
- const canInstallV256 = binaryHeader.swVersion === 568 ? semver.gte((_a = semver.coerce(deviceFirmwareVersion)) !== null && _a !== void 0 ? _a : '0.0.0', '5.5.0') : true;
3790
+ const canInstallV256 = binaryHeader.swVersion === 568 ? semver.gte((_b = semver.coerce(deviceFirmwareVersion)) !== null && _b !== void 0 ? _b : '0.0.0', '5.5.0') : true;
3726
3791
  const isCirculo = product.family === 'Circulo';
3727
3792
  const isDeviceLw1 = deviceSmmVersion < 259; // <v1.3
3728
3793
  const isV990 = deviceSmmVersion === 25344;
@@ -3732,7 +3797,7 @@ class MotionMasterReqResClient {
3732
3797
  const isAllowedBinaryVersion = allowedTargetSmmVersions.includes(binaryInformation.version);
3733
3798
  const isCirculo7 = isCirculo && product.group === 'Circulo 7';
3734
3799
  const isCirculo9 = isCirculo && product.group === 'Circulo 9';
3735
- const deviceSupportsStreaming = semver.gte((_b = semver.coerce(deviceFirmwareVersion)) !== null && _b !== void 0 ? _b : '0.0.0', '5.2.0');
3800
+ const deviceSupportsStreaming = semver.gte((_c = semver.coerce(deviceFirmwareVersion)) !== null && _c !== void 0 ? _c : '0.0.0', '5.2.0');
3736
3801
  const isAutomationAllowed = isCirculo &&
3737
3802
  isBinaryEncrypted &&
3738
3803
  isAllowedBinaryVersion &&
@@ -3760,7 +3825,8 @@ class MotionMasterReqResClient {
3760
3825
  });
3761
3826
  }
3762
3827
  /**
3763
- * Performs an automated, multi-step SMM software update to an encrypted firmware version on a Circulo device.
3828
+ * Performs an automated, multi-step SMM software update to an encrypted firmware version on a Circulo device,
3829
+ * or a Circulo-based ACTILINK-JD / JD Duo (judged by the underlying Circulo).
3764
3830
  *
3765
3831
  * Before installing the target binary, this method evaluates the device state by calling
3766
3832
  * {@link resolveSmmAutomatedUpdateConditions}. Based on the result, it determines which
@@ -3774,20 +3840,29 @@ class MotionMasterReqResClient {
3774
3840
  * - **v99.0 + Circulo 9**: v77.1 → target
3775
3841
  * - **v77.0 or v77.1**: target only
3776
3842
  *
3777
- * If the device does not support streaming, `commandTimeout` is automatically extended to 420,000 ms.
3843
+ * After each installation the SMM restarts. The next step starts only once the SMM reports the
3844
+ * version that was just installed (see {@link waitForSmmFirmwareVersion}), so a failed or
3845
+ * unfinished installation stops the sequence with an error instead of continuing blindly.
3846
+ *
3847
+ * Transfer settings that are not given are resolved from the device firmware with
3848
+ * {@link resolveSmmSoftwareUpdateTimings}. On firmware without fs-buffer streaming (< v5.2.0),
3849
+ * `chunkSize` is always 1000 and `commandTimeout` is at least 420,000 ms.
3778
3850
  *
3779
3851
  * @param deviceRef - Reference to the target device.
3780
3852
  * @param username - Username used for SMM authentication.
3781
3853
  * @param password - Password used for SMM authentication.
3782
3854
  * @param buffer - The target encrypted firmware binary as a `Uint8Array`.
3783
3855
  * @param crc - CRC32 checksum of the target firmware binary for integrity verification.
3784
- * @param chunkSize - (Optional) Size of each data chunk transmitted per step, in bytes. Defaults to 1000.
3785
- * @param commandTimeout - (Optional) Timeout for each OS command in ms. Automatically overridden to 420,000 ms
3786
- * if the device does not support streaming. Defaults to 30,000 ms.
3856
+ * @param chunkSize - (Optional) Size of each data chunk transmitted per step, in bytes.
3857
+ * Defaults to 1,000,000 with streaming (one command per file) and 1000 without.
3858
+ * @param commandTimeout - (Optional) Timeout for each OS command in ms.
3859
+ * Defaults to 180,000 ms with streaming and 420,000 ms without.
3787
3860
  * @param responsePollingInterval - (Optional) Polling interval for command responses in ms. Defaults to 1000 ms.
3788
- * @param fsBufferReadWriteTimeout - (Optional) Timeout for fs-buffer read/write operations in ms. Defaults to 120,000 ms.
3861
+ * @param fsBufferReadWriteTimeout - (Optional) Timeout for fs-buffer read/write operations in ms.
3862
+ * Defaults to 150,000 ms with streaming and 5000 ms without.
3789
3863
  *
3790
- * @returns An `Observable<void>` that completes when the full installation sequence finishes successfully.
3864
+ * @returns An `Observable` of {@link SmmAutomatedUpdateProgress} that reports each step and stage,
3865
+ * and completes when the full installation sequence finishes successfully.
3791
3866
  *
3792
3867
  * @throws {Error} With message `'Unknown product. Cannot perform SMM software update.'` if the device
3793
3868
  * serial number does not map to a known Somanet product.
@@ -3795,104 +3870,88 @@ class MotionMasterReqResClient {
3795
3870
  * e.g. the device is not a Circulo, the binary is not encrypted, the target version is
3796
3871
  * unsupported, the SoC firmware is below v5.5.0 (for SMM v2.56), or the device is
3797
3872
  * already on SMM v1.3+.
3798
- * @throws {Error} If any individual firmware installation step fails during execution.
3873
+ * @throws {Error} If any installation step fails, or the SMM does not report the installed version afterwards.
3799
3874
  */
3800
- updateSmmSoftwareToEncrypted(deviceRef, username, password, buffer, crc, chunkSize = 1000, commandTimeout = 30000, responsePollingInterval = 1000, fsBufferReadWriteTimeout = 120000) {
3801
- return new rxjs_1.Observable((subscriber) => {
3802
- (() => tslib_1.__awaiter(this, void 0, void 0, function* () {
3803
- /**
3804
- * We must wrap the whole anonymous function body in try/catch so the subscriber (in `catch`)
3805
- * knows when the error happens. Otherwise, the application will "explode" when the error occurs.
3806
- *
3807
- * Observable function is synchronous. When the error occurs, it will handle it via `error: (err) => {}`
3808
- * function. Here, that is not the case. The anonymous function is asynchronous and will update the
3809
- * subscriber only when we tell it to do so via `subscriber.<method>`. Error handling from the outside
3810
- * will not pick-up the errors from here as this is "fire-and-forget" type of function.
3811
- *
3812
- * @example
3813
- *
3814
- * ```typescript
3815
- * asyncHandler(async (req, res, next) => {
3816
- * somePromiseThatRejects().then(...) // ← no await, no .catch()
3817
- * // or a setTimeout, event emitter, etc.
3818
- *
3819
- * // handler finishes immediately
3820
- })
3821
- */
3822
- try {
3823
- const automatedInstall = yield this.resolveSmmAutomatedUpdateConditions(deviceRef, buffer);
3824
- if (!automatedInstall.isValidProduct) {
3825
- subscriber.error(new Error('Unknown product. Cannot perform SMM software update.'));
3826
- return;
3827
- }
3828
- let observables = [];
3829
- if (automatedInstall.isAutomationAllowed) {
3830
- const v990crc = 0xd1a3bb72;
3831
- const v990Buffer = yield Promise.resolve().then(() => tslib_1.__importStar(require('./smm/40701_DEBUG_Update_V99.0'))).then((m) => Uint8Array.from(atob(m.smmBinaryV990), (c, _) => { var _a; return (_a = c.codePointAt(0)) !== null && _a !== void 0 ? _a : 0; }));
3832
- const v770crc = 0xe290a20a;
3833
- const v770Buffer = yield Promise.resolve().then(() => tslib_1.__importStar(require('./smm/40701_DEBUG_Update_V77.0'))).then((m) => Uint8Array.from(atob(m.smmBinaryV770), (c, _) => { var _a; return (_a = c.codePointAt(0)) !== null && _a !== void 0 ? _a : 0; }));
3834
- const v771crc = 0xa4a11125;
3835
- const v771Buffer = yield Promise.resolve().then(() => tslib_1.__importStar(require('./smm/40701_DEBUG_Update_V77.1'))).then((m) => Uint8Array.from(atob(m.smmBinaryV771), (c, _) => { var _a; return (_a = c.codePointAt(0)) !== null && _a !== void 0 ? _a : 0; }));
3836
- const v990$ = (0, rxjs_1.defer)(() => this.updateSmmSoftware(deviceRef, username, password, new Uint8Array(v990Buffer), v990crc, chunkSize, commandTimeout, responsePollingInterval, fsBufferReadWriteTimeout));
3837
- const v770$ = (0, rxjs_1.defer)(() => this.updateSmmSoftware(deviceRef, username, password, new Uint8Array(v770Buffer), v770crc, chunkSize, commandTimeout, responsePollingInterval, fsBufferReadWriteTimeout));
3838
- const v771$ = (0, rxjs_1.defer)(() => this.updateSmmSoftware(deviceRef, username, password, new Uint8Array(v771Buffer), v771crc, chunkSize, commandTimeout, responsePollingInterval, fsBufferReadWriteTimeout));
3839
- const userVersion$ = (0, rxjs_1.defer)(() => this.updateSmmSoftware(deviceRef, username, password, buffer, crc, chunkSize, commandTimeout, responsePollingInterval, fsBufferReadWriteTimeout));
3840
- if (automatedInstall.isDeviceLw1) {
3841
- if (automatedInstall.isCirculo7) {
3842
- observables = [v990$, v770$, userVersion$];
3843
- }
3844
- else if (automatedInstall.isCirculo9) {
3845
- observables = [v990$, v771$, userVersion$];
3846
- }
3847
- }
3848
- else if (automatedInstall.isV990) {
3849
- if (automatedInstall.isCirculo7) {
3850
- observables = [v770$, userVersion$];
3851
- }
3852
- else if (automatedInstall.isCirculo9) {
3853
- observables = [v771$, userVersion$];
3854
- }
3855
- }
3856
- else if (automatedInstall.isV770 || automatedInstall.isV771) {
3857
- observables = [userVersion$];
3858
- }
3859
- (0, rxjs_1.concat)(...observables).subscribe({
3860
- next: () => {
3861
- subscriber.next();
3862
- },
3863
- error: (err) => {
3864
- subscriber.error(err);
3865
- },
3866
- complete: () => {
3867
- subscriber.complete();
3868
- },
3869
- });
3870
- }
3871
- else {
3872
- const reasons = [];
3873
- if (!automatedInstall.isCirculo) {
3874
- reasons.push('The device is not part of the Circulo family.');
3875
- }
3876
- if (!automatedInstall.isDeviceLw1) {
3877
- reasons.push('The device SMM software version is not lower than 1.3.');
3878
- }
3879
- if (!automatedInstall.isBinaryEncrypted) {
3880
- reasons.push('The SMM binary is not encrypted.');
3881
- }
3882
- if (!automatedInstall.isAllowedBinaryVersion) {
3883
- reasons.push('The target SMM software version is not supported for encrypted update.');
3884
- }
3885
- if (!automatedInstall.canInstallV256) {
3886
- reasons.push('The device firmware must be minimum v5.5.0 for installing SMM v2.56.');
3887
- }
3888
- subscriber.error(new Error(`The following conditions are not met for SMM software automated update: ${reasons.join(' ')}`));
3889
- }
3875
+ updateSmmSoftwareToEncrypted(deviceRef, username, password, buffer, crc, chunkSize, commandTimeout, responsePollingInterval = 1000, fsBufferReadWriteTimeout) {
3876
+ return (0, rxjs_1.defer)(() => (0, rxjs_1.from)(this.resolveSmmAutomatedUpdateConditions(deviceRef, buffer))).pipe((0, operators_1.mergeMap)((conditions) => {
3877
+ if (!conditions.isValidProduct) {
3878
+ throw new Error('Unknown product. Cannot perform SMM software update.');
3879
+ }
3880
+ if (!conditions.isAutomationAllowed) {
3881
+ const reasons = [];
3882
+ if (!conditions.isCirculo) {
3883
+ reasons.push('The device is not a Circulo or a Circulo-based ACTILINK-JD.');
3890
3884
  }
3891
- catch (err) {
3892
- subscriber.error(err);
3885
+ if (!conditions.isDeviceLw1 && !conditions.isV990 && !conditions.isV770 && !conditions.isV771) {
3886
+ reasons.push('The device SMM software version is not lower than 1.3.');
3893
3887
  }
3894
- }))();
3895
- });
3888
+ if (!conditions.isBinaryEncrypted) {
3889
+ reasons.push('The SMM binary is not encrypted.');
3890
+ }
3891
+ if (!conditions.isAllowedBinaryVersion) {
3892
+ reasons.push('The target SMM software version is not supported for encrypted update.');
3893
+ }
3894
+ if (!conditions.canInstallV256) {
3895
+ reasons.push('The device firmware must be minimum v5.5.0 for installing SMM v2.56.');
3896
+ }
3897
+ throw new Error(`The following conditions are not met for SMM software automated update: ${reasons.join(' ')}`);
3898
+ }
3899
+ const timings = (0, smm_1.resolveSmmSoftwareUpdateTimings)(conditions.deviceSupportsStreaming);
3900
+ const transfer = conditions.deviceSupportsStreaming
3901
+ ? {
3902
+ chunkSize: chunkSize !== null && chunkSize !== void 0 ? chunkSize : timings.chunkSize,
3903
+ commandTimeout: commandTimeout !== null && commandTimeout !== void 0 ? commandTimeout : timings.commandTimeout,
3904
+ fsBufferReadWriteTimeout: fsBufferReadWriteTimeout !== null && fsBufferReadWriteTimeout !== void 0 ? fsBufferReadWriteTimeout : timings.fsBufferReadWriteTimeout,
3905
+ }
3906
+ : {
3907
+ // Without streaming the fs-buffer is written before each command, which only works with 1000-byte chunks.
3908
+ chunkSize: timings.chunkSize,
3909
+ commandTimeout: Math.max(commandTimeout !== null && commandTimeout !== void 0 ? commandTimeout : 0, timings.commandTimeout),
3910
+ fsBufferReadWriteTimeout: fsBufferReadWriteTimeout !== null && fsBufferReadWriteTimeout !== void 0 ? fsBufferReadWriteTimeout : timings.fsBufferReadWriteTimeout,
3911
+ };
3912
+ const decode = (base64) => Uint8Array.from(atob(base64), (c) => { var _a; return (_a = c.codePointAt(0)) !== null && _a !== void 0 ? _a : 0; });
3913
+ const v990 = {
3914
+ version: 25344,
3915
+ load: () => Promise.resolve().then(() => tslib_1.__importStar(require('./smm/40701_DEBUG_Update_V99.0'))).then((m) => ({
3916
+ buffer: decode(m.smmBinaryV990),
3917
+ crc: 0xd1a3bb72,
3918
+ })),
3919
+ };
3920
+ const v770 = {
3921
+ version: 19712,
3922
+ load: () => Promise.resolve().then(() => tslib_1.__importStar(require('./smm/40701_DEBUG_Update_V77.0'))).then((m) => ({
3923
+ buffer: decode(m.smmBinaryV770),
3924
+ crc: 0xe290a20a,
3925
+ })),
3926
+ };
3927
+ const v771 = {
3928
+ version: 19713,
3929
+ load: () => Promise.resolve().then(() => tslib_1.__importStar(require('./smm/40701_DEBUG_Update_V77.1'))).then((m) => ({
3930
+ buffer: decode(m.smmBinaryV771),
3931
+ crc: 0xa4a11125,
3932
+ })),
3933
+ };
3934
+ const target = { version: conditions.binaryVersion, load: () => Promise.resolve({ buffer, crc }) };
3935
+ const intermediate = conditions.isCirculo7 ? v770 : conditions.isCirculo9 ? v771 : undefined;
3936
+ let plan = [];
3937
+ if (conditions.isV770 || conditions.isV771) {
3938
+ plan = [target];
3939
+ }
3940
+ else if (intermediate && conditions.isV990) {
3941
+ plan = [intermediate, target];
3942
+ }
3943
+ else if (intermediate && conditions.isDeviceLw1) {
3944
+ plan = [v990, intermediate, target];
3945
+ }
3946
+ if (plan.length === 0) {
3947
+ throw new Error('Cannot determine the SMM update path: the device is neither a Circulo 7 nor a Circulo 9.');
3948
+ }
3949
+ const steps = plan.length;
3950
+ return (0, rxjs_1.concat)(...plan.map(({ version, load }, i) => {
3951
+ const progress = { step: i + 1, steps, version };
3952
+ return (0, rxjs_1.concat)((0, rxjs_1.defer)(() => (0, rxjs_1.from)(load())).pipe((0, operators_1.mergeMap)((software) => this.updateSmmSoftwareWithProgress(deviceRef, username, password, software.buffer, software.crc, transfer.chunkSize, transfer.commandTimeout, responsePollingInterval, transfer.fsBufferReadWriteTimeout)), (0, operators_1.map)((install) => (Object.assign(Object.assign({}, progress), { stage: 'install', install })))), (0, rxjs_1.of)(Object.assign(Object.assign({}, progress), { stage: 'verify' })), (0, rxjs_1.defer)(() => this.waitForSmmFirmwareVersion(deviceRef, version)).pipe((0, operators_1.ignoreElements)()), (0, rxjs_1.of)(Object.assign(Object.assign({}, progress), { stage: 'installed' })));
3953
+ }));
3954
+ }));
3896
3955
  }
3897
3956
  /**
3898
3957
  * Transmits SMM parameters to the specified device.