tasmota-esp-web-tools
Version: 
Web tools for ESP devices
256 lines (227 loc) • 6.35 kB
text/typescript
import { ESPLoader, Logger } from "tasmota-webserial-esptool";
import {
  Build,
  FlashError,
  FlashState,
  Manifest,
  FlashStateType,
} from "./const";
import { getChipFamilyName } from "./util/chip-family-name";
import { sleep } from "./util/sleep";
export const flash = async (
  onEvent: (state: FlashState) => void,
  port: SerialPort,
  logger: Logger,
  manifestPath: string,
  eraseFirst: boolean,
  firmwareBuffer: Uint8Array,
) => {
  let manifest: Manifest;
  let build: Build | undefined;
  let chipFamily: ReturnType<typeof getChipFamilyName>;
  const fireStateEvent = (stateUpdate: FlashState) =>
    onEvent({
      ...stateUpdate,
      manifest,
      build,
      chipFamily,
    });
  var manifestProm = null;
  var manifestURL: string = "";
  try {
    manifestProm = JSON.parse(manifestPath);
  } catch {
    manifestURL = new URL(manifestPath, location.toString()).toString();
    manifestProm = fetch(manifestURL).then(
      (resp): Promise<Manifest> => resp.json(),
    );
  }
  const esploader = new ESPLoader(port, logger);
  // For debugging
  (window as any).esploader = esploader;
  fireStateEvent({
    state: FlashStateType.INITIALIZING,
    message: "Initializing...",
    details: { done: false },
  });
  try {
    await esploader.initialize();
  } catch (err: any) {
    logger.error(err);
    fireStateEvent({
      state: FlashStateType.ERROR,
      message:
        "Failed to initialize. Try resetting your device or holding the BOOT button while clicking INSTALL.",
      details: { error: FlashError.FAILED_INITIALIZING, details: err },
    });
    if (esploader.connected) {
      await esploader.disconnect();
    }
    return;
  }
  chipFamily = getChipFamilyName(esploader);
  fireStateEvent({
    state: FlashStateType.INITIALIZING,
    message: `Initialized. Found ${chipFamily}`,
    details: { done: true },
  });
  fireStateEvent({
    state: FlashStateType.MANIFEST,
    message: "Fetching manifest...",
    details: { done: false },
  });
  try {
    manifest = await manifestProm;
  } catch (err: any) {
    fireStateEvent({
      state: FlashStateType.ERROR,
      message: `Unable to fetch manifest: ${err}`,
      details: { error: FlashError.FAILED_MANIFEST_FETCH, details: err },
    });
    await esploader.disconnect();
    return;
  }
  build = manifest.builds.find((b) => b.chipFamily === chipFamily);
  fireStateEvent({
    state: FlashStateType.MANIFEST,
    message: `Found manifest for ${manifest.name}`,
    details: { done: true },
  });
  if (!build) {
    fireStateEvent({
      state: FlashStateType.ERROR,
      message: `Your ${chipFamily} board is not supported.`,
      details: { error: FlashError.NOT_SUPPORTED, details: chipFamily },
    });
    await esploader.disconnect();
    return;
  }
  fireStateEvent({
    state: FlashStateType.PREPARING,
    message: "Preparing installation...",
    details: { done: false },
  });
  const filePromises = build.parts.map(async (part) => {
    if (firmwareBuffer.length == 0) {
      //No firmware buffer provided, now download ...
      const url = new URL(part.path, manifestURL).toString();
      const resp = await fetch(url);
      if (!resp.ok) {
        throw new Error(
          `Downlading firmware ${part.path} failed: ${resp.status}`,
        );
      }
      return resp.arrayBuffer();
    }
    // buffer from local file upload
    return firmwareBuffer;
  });
  // Run the stub while we wait for files to download
  const espStub = await esploader.runStub();
  const files: (ArrayBuffer | Uint8Array)[] = [];
  let totalSize = 0;
  for (const prom of filePromises) {
    try {
      const data = await prom;
      files.push(data instanceof ArrayBuffer ? new Uint8Array(data) : data);
      totalSize += data.byteLength;
    } catch (err: any) {
      fireStateEvent({
        state: FlashStateType.ERROR,
        message: err.message,
        details: {
          error: FlashError.FAILED_FIRMWARE_DOWNLOAD,
          details: err.message,
        },
      });
      await esploader.disconnect();
      return;
    }
  }
  fireStateEvent({
    state: FlashStateType.PREPARING,
    message: "Installation prepared",
    details: { done: true },
  });
  if (eraseFirst) {
    fireStateEvent({
      state: FlashStateType.ERASING,
      message: "Erasing device...",
      details: { done: false },
    });
    await espStub.eraseFlash();
    fireStateEvent({
      state: FlashStateType.ERASING,
      message: "Device erased",
      details: { done: true },
    });
  }
  let lastPct = 0;
  fireStateEvent({
    state: FlashStateType.WRITING,
    message: `Writing progress: ${lastPct}%`,
    details: {
      bytesTotal: totalSize,
      bytesWritten: 0,
      percentage: lastPct,
    },
  });
  let totalWritten = 0;
  for (const part of build.parts) {
    const file = files.shift()!;
    const fileBuffer =
      file instanceof Uint8Array ? new Uint8Array(file).buffer : file;
    try {
      await espStub.flashData(
        fileBuffer as ArrayBuffer,
        (bytesWritten: number) => {
          const newPct = Math.floor(
            ((totalWritten + bytesWritten) / totalSize) * 100,
          );
          if (newPct === lastPct) {
            return;
          }
          lastPct = newPct;
          fireStateEvent({
            state: FlashStateType.WRITING,
            message: `Writing progress: ${newPct}%`,
            details: {
              bytesTotal: totalSize,
              bytesWritten: totalWritten + bytesWritten,
              percentage: newPct,
            },
          });
        },
        part.offset,
        true,
      );
    } catch (err: any) {
      fireStateEvent({
        state: FlashStateType.ERROR,
        message: err.message,
        details: { error: FlashError.WRITE_FAILED, details: err },
      });
      await esploader.disconnect();
      return;
    }
    totalWritten += file.byteLength;
  }
  fireStateEvent({
    state: FlashStateType.WRITING,
    message: "Writing complete",
    details: {
      bytesTotal: totalSize,
      bytesWritten: totalWritten,
      percentage: 100,
    },
  });
  await sleep(100);
  console.log("DISCONNECT");
  await esploader.disconnect();
  console.log("HARD RESET");
  await esploader.hardReset();
  fireStateEvent({
    state: FlashStateType.FINISHED,
    message: "All done!",
  });
};