Skip to content

Commit

Permalink
Add firmware flashing support
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielBaulig committed Feb 17, 2024
1 parent 5437ae0 commit b6c4d36
Show file tree
Hide file tree
Showing 9 changed files with 400 additions and 96 deletions.
3 changes: 3 additions & 0 deletions src/sleep.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
11 changes: 11 additions & 0 deletions src/ui/RadialProgress.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { radialProgess } from './RadialProgress.module.css';

export default function RadialProgess({progress}) {
progress = Math.max(0, Math.min(1, progress));
return <div
className={radialProgess}
>
<div style={{backgroundImage: `conic-gradient(black ${360*progress}deg, transparent 0deg)`}}></div>
{Math.round(progress * 100)}%
</div>;
}
22 changes: 22 additions & 0 deletions src/ui/RadialProgress.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.radialProgess {
display: flex;
position: relative;
justify-content: center;
align-items: center;
width: 80px;
height: 80px;
}

.radialProgess div {
content: '';
position: absolute;
inset: 8px;
border-radius: 50%;
padding: 8px;
background-image: conic-gradient(#000 360deg, transparent 0deg);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
220 changes: 220 additions & 0 deletions src/ui/components/FirmwareFlasher.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import Drawer from '../Drawer';
import Spinner from '../Spinner';
import RadialProgress from '../RadialProgress';

import css from '../css';

import { flexFill } from '../utility.module.css';

import { useState, useEffect, useReducer, useRef } from 'react';
import iif from '../../iif';
import sleep from '../../sleep';

import {Transport, ESPLoader} from 'esptool-js';

function useEspTool(port) {
const esptoolRef = useRef({});

const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'file_reading_start':
return {
...state,
reading: true,
};
case 'file_reading_complete':
return {
...state,
reading: false,
};
case 'upload_start':
return {
...state,
progress: 0,
uploading: true,
};
case 'upload_progress': {
const { progress } = action;
return {
...state,
progress,
};
}
case 'upload_complete':
return {
uploading: false,
progress: 1,
};
case 'upload_fail': {
const { error } = action;
return {
uploading: false,
error,
};
}
case 'flashing_start': {
return {
...state,
flashing: true,
done: false,
error: null,
};
}
case 'flashing_complete': {
return {
...state,
flashing: false,
done: true,
};
}
}
throw new Error(`Invalid action ${action.type}`);
}, {});

async function readFile(file) {
dispatch({ type: 'file_reading_start' });
return new Promise((resolve, reject) => {
const reader = new FileReader();

function onError() {
cleanup();
reject();
}
function onLoad(event) {
cleanup();
resolve(event.target.result);
}
function cleanup() {
reader.removeEventListener('load', onLoad);
reader.removeEventListener('error', onError);
}

reader.addEventListener('load', onLoad);
reader.addEventListener('error', onError);

reader.readAsBinaryString(file)
});
}

async function resetTransport(transport) {
// ESP Web Tools Install Button does this.
// Not entirely sure why, the commit adding this doens't speak
// to why this needs to happen, but I've been running into
// issues whith not being able to talk to Improv after flashing
// an MCU. So I'll see if this helps.
await transport.device.setSignals({
dataTerminalReady: false,
requestToSend: true,
});
await sleep(250);
await transport.device.setSignals({
dataTerminalReady: false,
requestToSend: false,
});
await sleep(250);
}

return [
state, {
async flash(file) {
const transport = new Transport(port, false);

try {
dispatch({ type: 'flashing_start' });
const loaderOptions = {
transport,
baudrate: 115200,
romBaudrate: 115200,
};

const loader = new ESPLoader(loaderOptions);

await loader.main();
await loader.flashId();

const data = await readFile(file);

const flashOptions = {
fileArray: [{data, address: 0}],
flashSize: "keep",
flashMode: "keep",
flashFreq: "keep",
eraseAll: false,
compress: true,
reportProgress: (index, written, total) => {
dispatch({ type: 'upload_progress', progress: written/total});
},
};
dispatch({ type: 'upload_start' });
await loader.writeFlash(flashOptions);
await resetTransport(transport);
dispatch({ type: 'upload_complete' });
dispatch({ type: 'flashing_complete' });
} catch(error) {
await resetTransport(transport);
dispatch({ type: 'upload_fail', error });
console.error(error);
}
},
},
];
}

async function showFilePicker(accept) {
return new Promise((resolve, reject) => {
const el = document.createElement('input');
el.style = 'display: none;';
el.type = 'file';
el.accept = accept;
document.body.appendChild(el);

function cleanup() {
el.removeEventListener('change', onChange);
el.removeEventListener('cancel', cleanup);
document.body.removeChild(el);
}
function onChange(event) {
cleanup();
const file = event.target.files[0];
if (!file) {
return resolve(null);
}
resolve(file);
}

function onCancel(event) {
cleanup();
resolve(null);
}

el.addEventListener('change', onChange);
el.addEventListener('cancel', onCancel);

el.click();
});
}

const fileExtensions = '.bin,.img,.hex,.elf';

export default function FirmwareFlasher({port, onFirmwareUpdateDone, label}) {
const [{flashing, progress, uploading, error, done}, esptool] = useEspTool(port);

return <>
{iif(uploading, <RadialProgress progress={progress} />)}
{iif(flashing && !uploading, <Spinner />, )}
{iif(error, <h3>⚠ Something went wrong.</h3>)}
{iif(done, <h3>Installed.</h3>)}
{iif(!flashing, <button className={flexFill} onClick={async () => {
const file = await showFilePicker(fileExtensions);
if (!file) {
return;
}
if (port.opened) {
// esptool.flash will open the port
await port.close();
}
await esptool.flash(file);
onFirmwareUpdateDone?.();
}}>{label}</button>)}
</>;
}
67 changes: 23 additions & 44 deletions src/ui/components/Improv.jsx → src/ui/components/ImprovWifi.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,15 @@ import { mdiWifiCheck, mdiWifiCog, mdiWifiCancel } from '@mdi/js';
import { useState } from 'react';

import { flex, flexFill } from '../utility.module.css';
import { link } from './Improv.module.css';
import { link } from './ImprovWifi.module.css';

export default function Improv({
export default function ImprovWifi({
initializing,
error,
provisioning,
initialized,
firmware,
chipFamily,
version,
nextUrl,
scanning,
provisioned,
ssids,
improv
Expand All @@ -30,19 +28,32 @@ export default function Improv({


if (!initialized && !initializing) {
return (
<EntitySection title="Wi-Fi" className={flex}>
<Icon className={css(flex, flexFill)} path={mdiWifiCancel} size={4}/>
return <>
<Icon
className={css(flex, flexFill)}
path={mdiWifiCancel}
size={4}
/>
<h3 className={flex}>No Improv detected</h3>
</EntitySection>
);
</>;
}

if (!initialized) {
if (!initialized || provisioning) {
return <Spinner className={css(flex, flexFill)} />;
}

let wifiSection = <>
if (isShowingWifiDialog) {
return <WifiSelectionComponent
scanning={scanning}
ssids={ssids}
onCancel={() => setShowWifiDialog(false)}
onConnect={async (ssid, password) => {
setShowWifiDialog(false);
await improv.provision(ssid, password, 60000);
}}
/>
}
return <>
<Icon
className={css(flex, flexFill)}
size={4}
Expand All @@ -69,36 +80,4 @@ export default function Improv({
</a>
)}
</>;

if (isShowingWifiDialog) {
wifiSection = <WifiSelectionComponent
ssids={ssids}
onCancel={() => setShowWifiDialog(false)}
onConnect={(ssid, password) => {
improv.provision(ssid, password, 60000);
setShowWifiDialog(false);
}}
/>
}

if (provisioning) {
wifiSection = <>
<Spinner />
</>;
}

return <>
<EntityCard title="Chip" className={flex}>
<h3>{chipFamily}</h3>
</EntityCard>
<EntityCard title="Firmware" className={flex}>
<h3>{firmware}</h3>
</EntityCard>
<EntityCard title="Version" className={flex}>
<h3>{version}</h3>
</EntityCard>
<EntitySection title="Wi-Fi" className={flex}>
{wifiSection}
</EntitySection>
</>;
}
File renamed without changes.
Loading

0 comments on commit b6c4d36

Please sign in to comment.