You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

266 lines
9.4 KiB

import JSZip from 'jszip';
import {
deviceModels,
FirmwareVersion,
} from '@app/components/Dashboard';
import type {
ISerialConnection,
Protobuf,
} from '@meshtastic/meshtasticjs';
import { EspLoader } from '@toit/esptool.js';
import type { ConfigPreset } from '../stores/appStore';
import type { Device } from '../stores/deviceStore';
type DeviceFlashingState = "doNotFlash" | "doFlash" | "idle" | "connecting" | "erasing" | "flashing" | "config" | "done" | "aborted" | "failed";
export type OverallFlashingState = "idle" | "downloading" | "busy" | "waiting";
type OverallFlashingCallback = (flashState: OverallFlashingState, progress?: number) => void;
let dataSections: {[index: string]: Uint8Array};
let zipFile: JSZip;
let configQueue: Protobuf.LocalConfig[] = [];
let firmwareToUse: FirmwareVersion;
let operations: FlashOperation[] = [];
let callback: OverallFlashingCallback;
let selectedDeviceModel: string;
let fullFlash: boolean;
export async function setup(configs: ConfigPreset[], deviceModelName: string, firmware: FirmwareVersion,
forceFullFlash: boolean, overallCallback: OverallFlashingCallback) {
dataSections = {};
selectedDeviceModel = deviceModelName;
callback = overallCallback;
console.log(`Firmware to use: ${firmware?.name} - ${firmware?.id}`);
firmwareToUse = firmware;
fullFlash = forceFullFlash;
await loadFirmware();
for(const c in configs) {
const config = configs[c];
for (let i = 0; i < config.count; i++) {
configQueue.push(config.config);
}
}
}
export async function nextBatch(devices: Device[], flashStates: FlashState[], deviceCallback: (flashState: FlashOperation) => void) {
callback("busy");
debugger;
devices = devices.filter((d, i) => flashStates[i].state == "doFlash");
flashStates = flashStates.filter(f => f.state == "doFlash");
if(devices.length > configQueue.length) {
console.warn("Too many devices!");
devices = devices.slice(0, configQueue.length);
}
operations = devices.map((dev, index) => new FlashOperation(dev, configQueue[index], deviceCallback));
operations.forEach((o, i) => o.state = flashStates[i]);
configQueue = configQueue.slice(operations.length)
console.log(`New config queue count: ${configQueue.length}`);
Promise.allSettled(operations.map(op => op.flash())).then(p => handleFlashingDone());
}
export function cancel() {
operations.forEach(o => o.cancel());
callback("idle");
}
function handleFlashingDone() {
if(configQueue.length == 0)
callback("idle");
else
callback("waiting");
}
function storeInDb(firmware: FirmwareVersion, file: ArrayBuffer) {
const db = indexedDB.open("firmwares");
db.onsuccess = () => {
debugger;
store(db.result, firmware, file);
};
db.onupgradeneeded = (ev) => {
const objStore = db.result.createObjectStore("files");
debugger;
objStore.transaction.oncomplete = () => store(db.result, firmware, file);
};
}
async function loadFromDb(firmware: FirmwareVersion) {
return new Promise<ArrayBuffer>((resolve, reject) => {
const db = indexedDB.open("firmwares");
db.onsuccess = () => {
const objStore = db.result.transaction("files", "readonly").objectStore("files");
const transaction = objStore.get(firmware.tag);
transaction.onsuccess = () => {
debugger;
resolve(transaction.result as ArrayBuffer);
};
transaction.onerror = reject;
}
});
}
function store(db: IDBDatabase, firmware: FirmwareVersion, file: ArrayBuffer) {
debugger;
const fileStore = db.transaction("files", "readwrite").objectStore("files");
fileStore.transaction.oncomplete = () => {
console.log("Successfully stored firmware in DB.");
}
fileStore.add(file, firmware.tag);
}
async function getZipFile() {
if(firmwareToUse.inLocalDb) {
const storedZip = await loadFromDb(firmwareToUse);
if(storedZip !== undefined)
return storedZip;
}
const zip = await fetch(`/firmware/${firmwareToUse.id}`);
const zipClone = zip.clone();
const contentLength = zip.headers.get("content-length");
const totalLength = contentLength ? parseInt(contentLength) : undefined;
const reader = zip.body!.getReader();
let bytesLoaded = 0;
while (true) {
const { done, value } = await reader.read();
if (done)
break;
bytesLoaded += value!.byteLength;
callback("downloading", totalLength ? bytesLoaded / totalLength : undefined);
}
const content = await zipClone.arrayBuffer();
storeInDb(firmwareToUse, content);
return content;
}
async function loadFirmware() {
console.log("Loading firmware");
// TODO: Error handling
const zip = await getZipFile();
zipFile = await JSZip.loadAsync(zip);
}
async function getSection(name: string): Promise<Uint8Array | undefined> {
if(!(name in dataSections)) {
const dataSection = await zipFile.file(name)?.async("uint8array");
if(dataSection === undefined) {
console.warn(`Firmware file ${name} not found.`);
return undefined;
}
dataSections[name] = dataSection;
}
return dataSections[name];
}
async function getFirmwareSections(deviceModel: string, alreadyFlashed: boolean) {
if(alreadyFlashed && !fullFlash) {
const updateFilename = `firmware-${deviceModel}-${firmwareToUse.tag}-update.bin`;
const mainUpdate = await getSection(updateFilename);
if(mainUpdate !== undefined) {
return [ { offset: 0x10000, data: mainUpdate } ];
}
}
const filename = `firmware-${deviceModel}-${firmwareToUse.tag}.bin`;
const main = await getSection(filename);
const bleoata = await getSection("bleota.bin");
const littlefs = await getSection(`littlefs-${firmwareToUse.tag}.bin`);
if(main === undefined || bleoata === undefined || littlefs === undefined)
throw "Missing firmware files.";
return [ // TODO: Is this correct for all device models?
{ offset: 0, data: main },
{ offset: 0x260000, data: bleoata },
{ offset: 0x300000, data: littlefs }
];
}
function autoDetectDeviceModel(port: SerialPort) {
const info = port.getInfo();
return deviceModels.find(d => info.usbVendorId == d.vendorId && info.usbProductId == d.productId)?.name;
}
export class FlashOperation {
public state: FlashState = { progress: 0, state: "idle" };
private loader?: EspLoader;
public constructor(public device: Device, public config: Protobuf.LocalConfig, public callback: (operation: FlashOperation) => void) {
}
public setState(state: DeviceFlashingState, progress = 0) {
console.log(`${this.device.nodes.get(this.device.hardware.myNodeNum)?.user
?.longName ?? "<Not flashed yet>"} flash state: ${state}`);
this.state = {state, progress};
this.callback(this);
}
public async flash() {
let port;
try {
debugger;
const updatePossible = this.device.nodes.get(this.device.hardware.myNodeNum) !== undefined;
port = await (this.device.connection! as ISerialConnection).freePort();
if(port === undefined)
throw "Port unavailable";
const deviceModel = selectedDeviceModel == "auto" ? autoDetectDeviceModel(port) : selectedDeviceModel;
if(deviceModel === undefined)
throw "Could not detect device model";
const info = port.getInfo();
console.log(`Device info: vendor ${info.usbVendorId}, product ${info.usbProductId}`);
const sections = await getFirmwareSections(deviceModel, updatePossible);
await port!.open({baudRate: 115200});
this.loader = new EspLoader(port!);
const loader = this.loader;
this.setState("connecting");
await loader.connect();
await loader.loadStub();
this.setState("erasing");
await new Promise(resolve => setTimeout(resolve, 2000));
await loader.eraseFlash();
const lul = this;
for (let i = 0; i < 3; i++) {
await loader.flashData(sections[i].data, sections[i].offset, function (idx, cnt) {
lul.setState("flashing", (1/3) * (i + idx/cnt));
});
}
}
catch (e) {
debugger;
this.setState("failed");
return;
}
finally {
await this.loader!.disconnect();
debugger;
}
port!.setSignals({requestToSend: true});
await new Promise(r => setTimeout(r, 100));
port!.setSignals({requestToSend: false});
await port!.close();
(this.device.connection! as ISerialConnection).connect({
port,
baudRate: 115200,
concurrentLogOutput: true
});
this.setState("done", 0);
}
public async cancel() {
debugger;
await this.loader?.disconnect();
this.setState("aborted");
}
}
export interface FlashState {
state: DeviceFlashingState,
progress: number,
};