Browse Source

Device selector

Malte Grimm 3 years ago
parent
commit
76d8914177
  1. 222
      src/components/Dashboard.tsx
  2. 93
      src/core/flashing/Flasher.ts
  3. 10
      src/core/stores/appStore.ts

222
src/components/Dashboard.tsx

@ -106,7 +106,7 @@ export const Dashboard = () => {
};
const DeviceList = ({devices, rootConfig, totalConfigCount}: {devices: Device[], rootConfig: ConfigPreset, totalConfigCount: number}) => {
const { setConnectDialogOpen, overallFlashingState, setOverallFlashingState, selectedFirmware, firmwareList, setFirmwareList } = useAppStore();
const { setConnectDialogOpen, overallFlashingState, setOverallFlashingState, selectedFirmware, selectedDeviceModel, firmwareList, setFirmwareList } = useAppStore();
const [deviceSelectedToFlash, setDeviceSelectedToFlash] = useState(devices.map(d => d.flashState));
// const [flashingState, setFlashingState]: any = useState([]);
const cancelButtonVisible = overallFlashingState.state != "idle";
@ -143,56 +143,62 @@ const DeviceList = ({devices, rootConfig, totalConfigCount}: {devices: Device[],
</Button>
</div>}
</ul>
<div className="flex gap-3">
<FirmwareSelection/>
{deviceSelectedToFlash.filter(d => d).length > 0 && <Button
className="gap-2 w-full"
disabled={totalConfigCount == 0 || overallFlashingState.state == "busy"}
onClick={async () => {
rootConfig.children[0].getFinalConfig(); // FIXME
if(overallFlashingState.state == "idle")
await setup(rootConfig.getAll(), firmware!, (state: OverallFlashingState, progress?: number) => {
if(state == 'busy') {
isStoredInDb(firmware!.tag).then(b => {
// All FirmwareVersion objects are immutable here so we'll have to re-create each entry
const newFirmwareList: FirmwareVersion[] = firmwareList.map(f => { return {
name: f.name,
tag: f.tag,
id: f.id,
inLocalDb: f == firmware ? b : f.inLocalDb
}});
setFirmwareList(newFirmwareList);
});
}
<div className="flex flex-col gap-3">
<div className="flex gap-3">
<DeviceModelSelection/>
<FirmwareSelection/>
</div>
<div className="flex gap-3">
<FirmwareSelection/>
{deviceSelectedToFlash.filter(d => d).length > 0 && <Button
className="gap-2 w-full"
disabled={totalConfigCount == 0 || overallFlashingState.state == "busy"}
onClick={async () => {
rootConfig.children[0].getFinalConfig(); // FIXME
if(overallFlashingState.state == "idle")
await setup(rootConfig.getAll(), selectedDeviceModel, firmware!, (state: OverallFlashingState, progress?: number) => {
if(state == 'busy') {
isStoredInDb(firmware!.tag).then(b => {
// All FirmwareVersion objects are immutable here so we'll have to re-create each entry
const newFirmwareList: FirmwareVersion[] = firmwareList.map(f => { return {
name: f.name,
tag: f.tag,
id: f.id,
inLocalDb: f == firmware ? b : f.inLocalDb
}});
setFirmwareList(newFirmwareList);
});
}
setOverallFlashingState({state, progress});
});
nextBatch(devices,
deviceSelectedToFlash, /* EXTREMELY HACKY -- FIX THIS */
(f)=> {
f.device.setFlashState(f.state);
deviceSelectedToFlash[devices.indexOf(f.device)] = f.state;
setDeviceSelectedToFlash(deviceSelectedToFlash);
setOverallFlashingState({state, progress});
});
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.state, overallFlashingState.progress)}
</Button>}
{cancelButtonVisible && <Button
className="ml-1 p-2"
variant={"destructive"}
onClick={() => {
if(!confirm("Cancel flashing?"))
return;
cancel();
}}
>
<XIcon/>
</Button>}
// flashingState[f.device.id] = f.state;
// setFlashingState(flashingState);
}
);
}}
>
{stateToText(overallFlashingState.state, overallFlashingState.progress)}
</Button>}
{cancelButtonVisible && <Button
className="ml-1 p-2"
variant={"destructive"}
onClick={() => {
if(!confirm("Cancel flashing?"))
return;
cancel();
}}
>
<XIcon/>
</Button>}
</div>
</div>
</div>
) : (
@ -342,7 +348,6 @@ const ConfigEntry = ({config, configPresetSelected, setConfigPresetSelected, edi
);
}
const DeviceSetupEntry = ({device, selectedToFlash, toggleSelectedToFlash, progressText}
:{device: Device, selectedToFlash: boolean, toggleSelectedToFlash: () => void, progressText: FlashState}) => {
// const [toBeFlashed, setToBeFlashed] = useState(device.selectedToFlash);
@ -476,6 +481,121 @@ const FirmwareSelection = () => {
);
}
type DeviceModel = {
displayName: string,
name: string,
vendorId: number,
productId: number
}
// TODO: Fill in remaining vendor and product IDs
export const deviceModels: DeviceModel[] = [
{
displayName: "Heltec v1",
name: "heltec-v1",
vendorId: -1,
productId: -1
},
{
displayName: "Heltec v2.0",
name: "heltec-v2.0",
vendorId: -1,
productId: -1
},
{
displayName: "Heltec v2.1",
name: "heltec-v2.1",
vendorId: -1,
productId: -1
},
{
displayName: "T-Beam v0.7",
name: "tbeam0.7",
vendorId: -1,
productId: -1
},
{
displayName: "T-Beam",
name: "tbeam",
vendorId: -1,
productId: -1
},
{
displayName: "T-Lora v1",
name: "tlora-v1",
vendorId: -1,
productId: -1
},
{
displayName: "T-Lora v1.3",
name: "tlora-v1_3",
vendorId: -1,
productId: -1
},
{
displayName: "T-Lora v2",
name: "tlora-v2",
vendorId: -1,
productId: -1
},
{
displayName: "T-Lora v2.1-1.6",
name: "tlora-v2-1-1.6",
vendorId: 6790,
productId: 21972
},
]
const DeviceModelSelection = () => {
const { selectedDeviceModel, setSelectedDeviceModel } = useAppStore();
let selectItems = [
<SelectItem key={"auto"} value={"auto"}>
{"Auto-detect device model"}
</SelectItem>,
<SelectSeparator/>
];
selectItems.push(...deviceModels.map(d =>
<SelectItem key={d.name} value={d.name}>
{d.displayName}
</SelectItem>
));
return (
<div className="flex gap-1 w-full">
<Select
onValueChange={setSelectedDeviceModel}
value={selectedDeviceModel}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{selectItems}
</SelectContent>
</Select>
{/* <Button
variant="outline"
className="ml-1 p-2"
title="Update firmware version list"
disabled={firmwareRefreshing}
onClick={() => {
setFirmwareRefreshing(true);
loadFirmwareList().then((list) => {
setFirmwareList(list.slice(0, 10));
setFirmwareRefreshing(false);
// TODO: What if download fails?
});
}}
>
<RefreshCwIcon size={20}/>
</Button> */}
</div>
);
}
export type FirmwareVersion = {
name: string,
tag: string,

93
src/core/flashing/Flasher.ts

@ -1,6 +1,9 @@
import JSZip from 'jszip';
import type { FirmwareVersion } from '@app/components/Dashboard';
import {
deviceModels,
FirmwareVersion,
} from '@app/components/Dashboard';
import type {
ISerialConnection,
Protobuf,
@ -15,17 +18,21 @@ export type OverallFlashingState = "idle" | "downloading" | "busy" | "waiting";
type OverallFlashingCallback = (flashState: OverallFlashingState, progress?: number) => void;
const sections: {data: Uint8Array, offset: number}[] = [ ];
let dataSections: {[index: string]: Uint8Array};
let zipFile: JSZip;
let configQueue: Protobuf.LocalConfig[] = [];
let firmwareToUse: FirmwareVersion;
let operations: FlashOperation[] = [];
let callback: OverallFlashingCallback;
let selectedDeviceModel: string;
export async function setup(configs: ConfigPreset[], firmware: FirmwareVersion, overallCallback: OverallFlashingCallback) {
export async function setup(configs: ConfigPreset[], deviceModelName: string, firmware: FirmwareVersion, overallCallback: OverallFlashingCallback) {
dataSections = {};
selectedDeviceModel = deviceModelName;
callback = overallCallback;
console.log(`Firmware to use: ${firmware?.name} - ${firmware?.id}`);
firmwareToUse = firmware;
await loadFirmware();
await loadFirmware();
for(const c in configs) {
const config = configs[c];
for (let i = 0; i < config.count; i++) {
@ -127,57 +134,48 @@ async function getZipFile() {
return content;
}
function getFileName() {
const device = "tlora-v2-1-1.6";
return `firmware-${device}-${firmwareToUse.tag}.bin`;
}
async function loadFirmware() {
console.warn("Loading firmware");
async function loadFirmware() {
console.log("Loading firmware");
// TODO: Error handling
const zip = await getZipFile();
const z = await JSZip.loadAsync(zip);
const filename = getFileName();
const mainFile = await z.file(filename)?.async("uint8array");
const bleoata = await z.file("bleota.bin")?.async("uint8array");
const littlefs = await z.file(`littlefs-${firmwareToUse.tag}.bin`)?.async("uint8array");
if(mainFile === undefined || bleoata === undefined || littlefs === undefined)
throw "Missing file(s)";
sections.push(...[
zipFile = await JSZip.loadAsync(zip);
}
async function getSection(name: string): Promise<Uint8Array> {
if(!(name in dataSections)) {
const dataSection = await zipFile.file(name)?.async("uint8array");
if(dataSection === undefined)
throw `File ${name} missing from firmware directory`;
dataSections[name] = dataSection;
}
return dataSections[name];
}
async function getFirmwareSections(deviceModel: string) {
debugger;
const filename = `firmware-${deviceModel}-${firmwareToUse.tag}.bin`;
const mainFile = await getSection(filename);
const bleoata = await getSection("bleota.bin");
const littlefs = await getSection(`littlefs-${firmwareToUse.tag}.bin`);
return [ // TODO: Is this correct for all device models?
{ offset: 0, data: mainFile },
{ offset: 0x260000, data: bleoata },
{ offset: 0x300000, data: littlefs }
]);
}
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}`);
];
}
function getDeviceType(port: SerialPort) {
function autoDetectDeviceModel(port: SerialPort) {
const info = port.getInfo();
switch(info.usbVendorId) {
case 6790:
switch(info.usbProductId) {
case 21972:
return "tlora-v2-1-1.6";
}
break;
}
return undefined;
return deviceModels.find(d => info.usbVendorId == d.vendorId && info.usbProductId == d.productId)?.name;
}
export class FlashOperation {
public state: FlashState = { progress: 0, state: "idle" };
public state: FlashState = { progress: 0, state: "idle" };
private loader?: EspLoader;
public constructor(public device: Device, public config: Protobuf.LocalConfig, public callback: (operation: FlashOperation) => void) {
@ -195,15 +193,22 @@ export class FlashOperation {
let port;
try {
port = await (this.device.connection! as ISerialConnection).freePort();
const info = port?.getInfo();
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);
await port!.open({baudRate: 115200});
this.loader = new EspLoader(port!);
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 new Promise(resolve => setTimeout(resolve, 2000));
await loader.eraseFlash();
const lul = this;
for (let i = 0; i < 3; i++) {

10
src/core/stores/appStore.ts

@ -185,6 +185,7 @@ interface AppState {
firmwareRefreshing: boolean;
firmwareList: FirmwareVersion[];
selectedFirmware: string;
selectedDeviceModel: string;
setRasterSources: (sources: RasterSource[]) => void;
addRasterSource: (source: RasterSource) => void;
@ -203,6 +204,7 @@ interface AppState {
setFirmwareRefreshing: (state: boolean) => void;
setFirmwareList: (state: FirmwareVersion[]) => void;
setSelectedFirmware: (state: string) => void;
setSelectedDeviceModel: (state: string) => void;
}
export const useAppStore = create<AppState>()((set) => ({
@ -221,6 +223,7 @@ export const useAppStore = create<AppState>()((set) => ({
firmwareRefreshing: false,
firmwareList: loadFirmwareListFromStorage(),
selectedFirmware: "latest",
selectedDeviceModel: "auto",
setRasterSources: (sources: RasterSource[]) => {
set(
@ -327,4 +330,11 @@ export const useAppStore = create<AppState>()((set) => ({
})
)
},
setSelectedDeviceModel(state: string) {
set(
produce<AppState>((draft) => {
draft.selectedDeviceModel = state;
})
)
},
}));
Loading…
Cancel
Save