@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
238 lines (211 loc) • 8.17 kB
text/typescript
import { log } from "@ledgerhq/logs";
import { MCUNotGenuineToDashboard } from "@ledgerhq/errors";
import { Observable, from, of, EMPTY, concat, throwError } from "rxjs";
import type { DeviceVersion, FinalFirmware, McuVersion } from "@ledgerhq/types-live";
import { concatMap, delay, filter, map, mergeMap, throttleTime } from "rxjs/operators";
import semver from "semver";
import ManagerAPI from "../manager/api";
import { withDevicePolling, withDevice } from "./deviceAccess";
import { getProviderId } from "../manager/provider";
import getDeviceInfo from "./getDeviceInfo";
import {
mcuOutdated,
mcuNotGenuine,
followDeviceRepair,
followDeviceUpdate,
} from "../deviceWordings";
import { getDeviceRunningMode } from "./getDeviceRunningMode";
import { fetchMcusUseCase } from "../device/use-cases/fetchMcusUseCase";
const wait2s = of({
type: "wait",
}).pipe(delay(2000));
export const repairChoices = [
{
id: "mcuOutdated",
label: mcuOutdated,
forceMCU: "0.7",
},
{
id: "mcuNotGenuine",
label: mcuNotGenuine,
forceMCU: "0.7",
},
{
id: "followDeviceRepair",
label: followDeviceRepair,
forceMCU: "0.9",
},
{
id: "followDeviceUpdate",
label: followDeviceUpdate,
forceMCU: "0.9",
},
];
const repair = (
deviceId: string,
forceMCU_?: string | null,
): Observable<{
progress: number;
}> => {
log("hw", "firmwareUpdate-repair");
const mcusPromise = fetchMcusUseCase();
const withDeviceInfo = withDevicePolling(deviceId)(
transport => from(getDeviceInfo(transport)),
() => true, // accept all errors. we're waiting forever condition that make getDeviceInfo work
);
const waitForBootloader = withDeviceInfo.pipe(
concatMap(deviceInfo => (deviceInfo.isBootloader ? EMPTY : concat(wait2s, waitForBootloader))),
);
const loop = (forceMCU?: string | null | undefined) =>
concat(
withDeviceInfo.pipe(
concatMap(deviceInfo => {
const installMcu = (version: string) =>
withDevice(deviceId)(transport =>
ManagerAPI.installMcu(transport, "mcu", {
targetId: deviceInfo.targetId,
version,
}),
);
if (!deviceInfo.isBootloader) {
// finish earlier
return EMPTY;
}
// This is a special case where user is in firmware 1.3.1
// and the device shows MCU Not Genuine.
// User needs to press both keys three times to go back to dashboard
// and continue the update process
if (
forceMCU &&
forceMCU === "0.7" &&
(deviceInfo.majMin === "0.6" || deviceInfo.majMin === "0.7")
) {
// finish earlier
return throwError(() => new MCUNotGenuineToDashboard());
}
if (forceMCU) {
return concat(installMcu(forceMCU), wait2s, loop());
}
switch (deviceInfo.majMin) {
case "0.0":
return concat(installMcu("0.6"), wait2s, loop());
case "0.6":
return installMcu("1.5");
case "0.7":
return installMcu("1.6");
case "0.9":
return installMcu("1.7");
default:
return from(mcusPromise).pipe(
concatMap(mcus => {
let next;
const { seVersion, seTargetId, mcuBlVersion } = deviceInfo;
// This is a special case where a user with LNX version >= 2.0.0
// comes back with a broken updated device. We need to be able
// to patch MCU or Bootloader if needed
if (seVersion && seTargetId) {
log("hw", "firmwareUpdate-repair seVersion and seTargetId found", {
seVersion,
seTargetId,
});
const provider = getProviderId(deviceInfo);
/**
* filter the MCUs that are available on the provider and
* have a "from_bootloader_version" different from "none"
* */
const availableMcus = mcus.filter(
mcu =>
mcu.providers.includes(provider) && mcu.from_bootloader_version !== "none",
);
log("hw", `firmwareUpdate-repair available mcus on provider ${provider}`, {
availableMcus,
});
return from(
ManagerAPI.getDeviceVersion(seTargetId, getProviderId(deviceInfo)),
).pipe(
mergeMap((deviceVersion: DeviceVersion) =>
from(
ManagerAPI.getCurrentFirmware({
deviceId: deviceVersion.id,
version: seVersion,
provider: getProviderId(deviceInfo),
}),
),
),
mergeMap((finalFirmware: FinalFirmware) => {
log("hw", "firmwareUpdate-repair got final firmware", {
finalFirmware,
});
const mcu = ManagerAPI.findBestMCU(
availableMcus.filter(({ id }: McuVersion) =>
finalFirmware.mcu_versions.includes(id),
),
);
log("hw", "firmwareUpdate-repair got mcu", { mcu });
if (!mcu) return EMPTY;
const expectedBootloaderVersion = semver.coerce(
mcu.from_bootloader_version,
)?.version;
const currentBootloaderVersion = semver.coerce(mcuBlVersion)?.version;
log("hw", "firmwareUpdate-repair bootloader versions", {
currentBootloaderVersion,
expectedBootloaderVersion,
});
if (expectedBootloaderVersion === currentBootloaderVersion) {
next = mcu;
log("hw", "firmwareUpdate-repair bootloader versions are the same", {
next,
});
} else {
next = {
name: mcu.from_bootloader_version,
};
log("hw", "firmwareUpdate-repair bootloader versions are different", {
next,
});
}
return installMcu(next.name);
}),
);
} else {
next = ManagerAPI.findBestMCU(
ManagerAPI.compatibleMCUForDeviceInfo(
mcus,
deviceInfo,
getProviderId(deviceInfo),
),
);
if (next) return installMcu(next.name);
}
return EMPTY;
}),
);
}
}),
),
from(
getDeviceRunningMode({
deviceId,
unresponsiveTimeoutMs: 4000,
cantOpenDeviceRetryLimit: 2,
}),
).pipe(
mergeMap(result => {
if (result.type === "bootloaderMode") {
return loop(forceMCU);
} else {
return EMPTY;
}
}),
),
);
// TODO ideally we should race waitForBootloader with an event "display-bootloader-reboot", it should be a delayed event that is not emitted if waitForBootloader is fast enough..
return concat(waitForBootloader, loop(forceMCU_)).pipe(
filter((e: any) => e.type === "bulk-progress"),
map(e => ({
progress: e.progress,
})),
throttleTime(100),
);
};
export default repair;