Browse Source

Serial flashing

Malte Grimm 3 years ago
parent
commit
60ccfca020
  1. 117
      src/components/Dashboard.tsx
  2. 168
      src/core/flashing/Flasher.ts
  3. 30
      src/core/stores/appStore.ts
  4. 1
      src/core/stores/deviceStore.ts

117
src/components/Dashboard.tsx

@ -6,8 +6,7 @@ import {
Edit3Icon,
ListPlusIcon,
UsersIcon,
MapPinIcon,
CalendarIcon,
XIcon,
BluetoothIcon,
UsbIcon,
NetworkIcon
@ -23,7 +22,7 @@ import { DeviceConfig } from "@app/pages/Config/DeviceConfig";
import { ISerialConnection, Protobuf } from "@meshtastic/meshtasticjs";
import { SidebarButton } from "./UI/Sidebar/sidebarButton";
import { ConfigSelectButton } from "./UI/ConfigSelectButton";
import { Flasher, FlashOperation, FlashState } from "@app/core/flashing/Flasher";
import { setup, FlashOperation, FlashState, nextBatch, OverallFlashingState } from "@app/core/flashing/Flasher";
import { randId } from "@app/core/utils/randId";
import { subscribeAll } from "@app/core/subscriptions";
import { connect } from "http2";
@ -33,6 +32,9 @@ let initTest: boolean = false;
export const Dashboard = () => {
let { configPresetRoot, configPresetSelected } : {configPresetRoot: ConfigPreset, configPresetSelected?: ConfigPreset} = useAppStore();
const { addDevice, getDevices } = useDeviceStore();
const getTotalConfigCount = (c: ConfigPreset): number => c.children.map(child => getTotalConfigCount(child)).reduce((prev, cur) => prev + cur, c.count);
const [ totalConfigCount, setTotalConfigCount ] = useState(configPresetRoot.getTotalConfigCount());
console.log(`${totalConfigCount} :: ${useAppStore().overallFlashingState}`);
const devices: Device[] = useMemo(() => getDevices(), [getDevices]);
const onConnect = async (port: SerialPort) => {
const id = randId();
@ -71,8 +73,8 @@ export const Dashboard = () => {
<div className="flex w-full h-full gap-3">
<div className="flex flex-col w-[400px] h-full">
<DeviceList devices={devices} rootConfig={configPresetRoot}/>
<ConfigList rootConfig={configPresetRoot}/>
<DeviceList devices={devices} rootConfig={configPresetRoot} totalConfigCount={totalConfigCount}/>
<ConfigList rootConfig={configPresetRoot} setTotalConfigCountDiff={(diff) => setTotalConfigCount(totalConfigCount + diff)}/>
</div>
<div className="flex w-full h-full"><DeviceConfig key={configPresetSelected?.name}/></div>
</div>
@ -80,11 +82,12 @@ export const Dashboard = () => {
);
};
const DeviceList = ({devices, rootConfig}: {devices: Device[], rootConfig: ConfigPreset}) => {
const { setConnectDialogOpen, flasher } = useAppStore();
const [deviceSelectedToFlash, setDeviceSelectedToFlash] = useState(devices.map(d => d.flashState));
const DeviceList = ({devices, rootConfig, totalConfigCount}: {devices: Device[], rootConfig: ConfigPreset, totalConfigCount: number}) => {
const { setConnectDialogOpen, overallFlashingState, setOverallFlashingState } = useAppStore();
const [deviceSelectedToFlash, setDeviceSelectedToFlash] = useState(devices.map(d => d.flashState));
// const [flashingState, setFlashingState]: any = useState([]);
const cancelButtonVisible = overallFlashingState != "idle";
return (
<div className="flex rounded-md border border-dashed border-slate-200 h-1/2 p-3 mb-2 dark:border-slate-700">
{devices.length ? (
@ -104,22 +107,46 @@ const DeviceList = ({devices, rootConfig}: {devices: Device[], rootConfig: Confi
/>
);
})}
{<div className="m-auto flex flex-col gap-3 text-center">
<Button
className="mt-3 gap-2"
variant="outline"
onClick={() => setConnectDialogOpen(true)}
>
<PlusIcon size={16} />
New Connection
</Button>
</div>}
</ul>
{deviceSelectedToFlash.filter(d => d).length > 0 && <Button
className="gap-2"
onClick={() => {
flasher.flashAll(devices, rootConfig.children, (f)=> {
f.device.setFlashState(f.state);
deviceSelectedToFlash[devices.indexOf(f.device)] = f.state;
setDeviceSelectedToFlash(deviceSelectedToFlash);
// flashingState[f.device.id] = f.state;
// setFlashingState(flashingState);
debugger;
});
}}
>
Flash
</Button>}
<div className="flex">
{deviceSelectedToFlash.filter(d => d).length > 0 && <Button
className="gap-2 w-full"
disabled={totalConfigCount == 0 || overallFlashingState == "busy"}
onClick={async () => {
if(overallFlashingState == "idle")
await setup(rootConfig.getAll(), setOverallFlashingState);
nextBatch(devices,
deviceSelectedToFlash, /* EXTREMELY HACKY -- FIX THIS */
(f)=> {
f.device.setFlashState(f.state);
deviceSelectedToFlash[devices.indexOf(f.device)] = f.state;
setDeviceSelectedToFlash(deviceSelectedToFlash);
// flashingState[f.device.id] = f.state;
// setFlashingState(flashingState);
}
);
}}
>
{stateToText(overallFlashingState)}
</Button>}
{cancelButtonVisible && <Button
className="ml-1 p-2"
variant={"destructive"}
>
<XIcon/>
</Button>}
</div>
</div>
) : (
<div className="m-auto flex flex-col gap-3 text-center">
@ -140,14 +167,14 @@ const DeviceList = ({devices, rootConfig}: {devices: Device[], rootConfig: Confi
};
const ConfigList = ({rootConfig}: {rootConfig: ConfigPreset}) => {
const ConfigList = ({rootConfig, setTotalConfigCountDiff}: {rootConfig: ConfigPreset, setTotalConfigCountDiff: (val: number) => void}) => {
const { configPresetRoot, setConfigPresetRoot, configPresetSelected, setConfigPresetSelected } = useAppStore();
const [ editSelected, setEditSelected ] = useState(false);
if(configPresetSelected === undefined) {
setConfigPresetSelected(configPresetRoot);
return (<div/>);
}
}
return (
<div className="flex flex-col rounded-md border border-dashed border-slate-200 h-1/2 p-3 mb-2 dark:border-slate-700">
@ -198,6 +225,7 @@ const ConfigList = ({rootConfig}: {rootConfig: ConfigPreset}) => {
configPresetSelected={configPresetSelected}
setConfigPresetSelected={setConfigPresetSelected}
editSelected={editSelected}
onConfigCountChanged={(val, diff) => setTotalConfigCountDiff(diff)}
onEditDone={(val) => {
configPresetSelected.name = val;
setEditSelected(false);
@ -220,15 +248,20 @@ const ConfigList = ({rootConfig}: {rootConfig: ConfigPreset}) => {
};
const ConfigEntry = ({config, configPresetSelected, setConfigPresetSelected, editSelected, onEditDone}:
{config: ConfigPreset, configPresetSelected: ConfigPreset, setConfigPresetSelected: (selection: ConfigPreset) => void, editSelected: boolean, onEditDone: (value: string) => void}) => {
const ConfigEntry = ({config, configPresetSelected, setConfigPresetSelected, editSelected, onEditDone, onConfigCountChanged}:
{config: ConfigPreset,
configPresetSelected: ConfigPreset,
setConfigPresetSelected: (selection: ConfigPreset) => void,
editSelected: boolean, onEditDone: (value: string) => void,
onConfigCountChanged: (val: number, diff: number) => void
}) => {
const [configCount, setConfigCount] = useState(config.count);
return (
<div>
<ConfigSelectButton
label={config.name}
active={config == configPresetSelected}
setValue={(value) => {setConfigCount(value);}}
setValue={(value) => {const diff = value - config.count; config.count = value; setConfigCount(value); onConfigCountChanged(value, diff);}}
value={configCount}
editing={editSelected && config == configPresetSelected}
onClick={() => setConfigPresetSelected(config)}
@ -236,7 +269,14 @@ const ConfigEntry = ({config, configPresetSelected, setConfigPresetSelected, edi
/>
<div className="ml-[20px]">
{config.children.map(c =>
(<ConfigEntry config={c} configPresetSelected={configPresetSelected} setConfigPresetSelected={setConfigPresetSelected} editSelected={editSelected} onEditDone={onEditDone}/>)
(<ConfigEntry
config={c}
configPresetSelected={configPresetSelected}
setConfigPresetSelected={setConfigPresetSelected}
editSelected={editSelected}
onEditDone={onEditDone}
onConfigCountChanged={onConfigCountChanged}
/>)
)}
</div>
</div>
@ -293,7 +333,7 @@ const DeviceSetupEntry = ({device, selectedToFlash, toggleSelectedToFlash, progr
className="w-full gap-2 h-8"
onClick={() => toggleSelectedToFlash()}
>
{stateToText(progressText)}
{deviceStateToText(progressText)} {/* TODO: Replace with inner text */}
</Button>
</div>
@ -303,7 +343,7 @@ const DeviceSetupEntry = ({device, selectedToFlash, toggleSelectedToFlash, progr
);
}
function stateToText(state: FlashState) {
function deviceStateToText(state: FlashState) {
switch(state.state) {
case "doNotFlash":
return "Disabled";
@ -318,4 +358,17 @@ function stateToText(state: FlashState) {
default:
return state.state;
}
}
function stateToText(state: OverallFlashingState) {
switch(state) {
case "idle":
return "Flash";
case "busy":
return "In Progress...";
case "waiting":
return "Continue";
default:
state;
}
}

168
src/core/flashing/Flasher.ts

@ -1,91 +1,103 @@
import type { ISerialConnection, Protobuf } from "@meshtastic/meshtasticjs";
import type { ConfigPreset } from "../stores/appStore";
import { ConfigPreset, useAppStore } from "../stores/appStore";
import type { Device } from "../stores/deviceStore";
import { EspLoader } from "@toit/esptool.js";
type State = "doNotFlash" | "doFlash" | "idle" | "connecting" | "erasing" | "flashing" | "config" | "done";
export class Flasher {
// TODO: Add choice for different firmwares
public sections: {data: Uint8Array, offset: number}[] = [ ];
public firmwareAvailable: boolean = false;
public async flashAll(devices: Device[], configs: ConfigPreset[], callback: (flashState: FlashOperation) => void) {
if(!this.firmwareAvailable)
await this.init();
const configMap: Protobuf.LocalConfig[] = [];
for(const c in configs) {
const config = configs[c];
for (let i = 0; i < config.count; i++) {
configMap.push(config.config);
}
}
if(devices.length < configMap.length)
throw "Not enough devices"
const operations = configMap.map((cfg, index) => new FlashOperation(devices[index], cfg, callback));
operations.map(op => this.flashDevice(op));
}
type DeviceFlashingState = "doNotFlash" | "doFlash" | "idle" | "connecting" | "erasing" | "flashing" | "config" | "done";
export type OverallFlashingState = "idle" | "busy" | "waiting";
const sections: {data: Uint8Array, offset: number}[] = [ ];
let configQueue: Protobuf.LocalConfig[] = [];
let firmwareAvailable: boolean = false;
let callback: (flashState: OverallFlashingState) => void;
export async function setup(configs: ConfigPreset[], overallCallback: (flashState: OverallFlashingState) => void) {
callback = overallCallback;
if(!firmwareAvailable)
await init();
for(const c in configs) {
const config = configs[c];
for (let i = 0; i < config.count; i++) {
configQueue.push(config.config);
}
}
}
public constructor() {
// this.Init();
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);
}
const 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 => flashDevice(op))).then(p => handleFlashingDone());
}
public async init() {
this.firmwareAvailable = true;
await this.downloadFirmware("firmware-tlora-v2-1-1.6-2.0.6.97fd5cf.bin", 0, 0);
await this.downloadFirmware("bleota.bin", 2490368, 1);
await this.downloadFirmware("littlefs-2.0.6.97fd5cf.bin", 3145728, 2);
}
function handleFlashingDone() {
if(configQueue.length == 0)
callback("idle");
else
callback("waiting");
}
private async downloadFirmware(path: string, offset: number, slot: number) {
const req = await fetch(path);
if(!req.ok)
throw "failed";
const hmm = await req.arrayBuffer();
const data = new Uint8Array(hmm);
this.sections[slot] = {data, offset};
console.log(`Downloaded ${path}`);
}
async function init() {
firmwareAvailable = true;
await downloadFirmware("firmware-tlora-v2-1-1.6-2.0.6.97fd5cf.bin", 0, 0);
await downloadFirmware("bleota.bin", 2490368, 1);
await downloadFirmware("littlefs-2.0.6.97fd5cf.bin", 3145728, 2);
}
async function downloadFirmware(path: string, offset: number, slot: number) {
const req = await fetch(path);
if(!req.ok)
throw "failed";
const hmm = await req.arrayBuffer();
const data = new Uint8Array(hmm);
sections[slot] = {data, offset};
console.log(`Downloaded ${path}`);
}
public async flashDevice(state: FlashOperation) {
const sections = this.sections;
const port = await (state.device.connection! as ISerialConnection).freePort();
await port!.open({baudRate: 115200});
const loader = new EspLoader(port!);
state.setState("connecting");
await loader.connect();
await loader.loadStub();
state.setState("erasing");
const t = this;
try {
await loader.eraseFlash();
for (let i = 0; i < 3; i++) {
await loader.flashData(sections[i].data, sections[i].offset, function (idx, cnt) {
state.setState("flashing", (1/3) * idx/cnt);
});
}
} finally {
await loader.disconnect();
}
port!.setSignals({requestToSend: true});
await new Promise(r => setTimeout(r, 100));
port!.setSignals({requestToSend: false});
await port!.close();
(state.device.connection! as ISerialConnection).connect({
port,
baudRate: 115200,
concurrentLogOutput: true
});
state.setState("done", 0);
}
async function flashDevice(state: FlashOperation) {
const port = await (state.device.connection! as ISerialConnection).freePort();
await port!.open({baudRate: 115200});
const loader = new EspLoader(port!);
state.setState("connecting");
await loader.connect();
await loader.loadStub();
state.setState("erasing");
await new Promise(resolve => setTimeout(resolve, 2000));
// try {
// await loader.eraseFlash();
// for (let i = 0; i < 3; i++) {
// await loader.flashData(sections[i].data, sections[i].offset, function (idx, cnt) {
// state.setState("flashing", (1/3) * idx/cnt);
// });
// }
// } finally {
// await loader.disconnect();
// }
// port!.setSignals({requestToSend: true});
// await new Promise(r => setTimeout(r, 100));
// port!.setSignals({requestToSend: false});
// await port!.close();
// (state.device.connection! as ISerialConnection).connect({
// port,
// baudRate: 115200,
// concurrentLogOutput: true
// });
state.setState("done", 0);
}
export class FlashOperation {
public state: FlashState = { progress: 0, state: "idle" };
@ -94,7 +106,9 @@ export class FlashOperation {
}
public setState(state: State, progress = 0) {
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);
}
@ -102,6 +116,6 @@ export class FlashOperation {
}
export interface FlashState {
state: State,
state: DeviceFlashingState,
progress: number,
};

30
src/core/stores/appStore.ts

@ -1,7 +1,7 @@
import { produce } from "immer";
import { create } from "zustand";
import { Protobuf } from "@meshtastic/meshtasticjs";
import { Flasher } from "../flashing/Flasher";
import { OverallFlashingState } from "../flashing/Flasher";
export interface RasterSource {
enabled: boolean;
@ -50,6 +50,18 @@ export class ConfigPreset {
URL.revokeObjectURL(url);
}
public getTotalConfigCount(): number {
return this.children.map(child => child.getTotalConfigCount()).reduce((prev, cur) => prev + cur, this.count);
}
public getAll(): ConfigPreset[] {
const configs: ConfigPreset[] = [ this ];
this.children.forEach(c =>
configs.push(...c.getAll())
);
return configs;
}
private getConfigTreeJSON(): string {
if(this.parent) {
return this.parent.getConfigTreeJSON();
@ -82,7 +94,7 @@ export class ConfigPreset {
public static importConfigTree() {
}
}
public restoreChildConnections() {
this.children.forEach((c) => {
@ -106,7 +118,7 @@ interface AppState {
connectDialogOpen: boolean;
configPresetRoot: ConfigPreset;
configPresetSelected: ConfigPreset | undefined;
flasher: Flasher;
overallFlashingState: OverallFlashingState;
setRasterSources: (sources: RasterSource[]) => void;
addRasterSource: (source: RasterSource) => void;
@ -121,6 +133,7 @@ interface AppState {
setConnectDialogOpen: (open: boolean) => void;
setConfigPresetRoot: (root: ConfigPreset) => void;
setConfigPresetSelected: (selection: ConfigPreset) => void;
setOverallFlashingState: (state: OverallFlashingState) => void;
}
export const useAppStore = create<AppState>()((set) => ({
@ -134,7 +147,7 @@ export const useAppStore = create<AppState>()((set) => ({
connectDialogOpen: false,
configPresetRoot: ConfigPreset.loadOrCreate(),
configPresetSelected: undefined,
flasher: new Flasher(),
overallFlashingState: "idle",
setRasterSources: (sources: RasterSource[]) => {
set(
@ -211,5 +224,12 @@ export const useAppStore = create<AppState>()((set) => ({
draft.configPresetSelected = selection;
})
)
}
},
setOverallFlashingState: (state: OverallFlashingState) => {
set(
produce<AppState>((draft) => {
draft.overallFlashingState = state;
})
)
},
}));

1
src/core/stores/deviceStore.ts

@ -341,6 +341,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
debugger;
if (device) {
device.flashState = state;
}

Loading…
Cancel
Save