Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NRFutil style DFU support. A bit flakey, but seemingly functional #101

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion components/Flash.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
</div>
<div id="flash-modal" tabindex="-1" aria-hidden="true"
class="dark hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<TargetsUf2 v-if="['nrf52840', 'rp2040'].includes(deviceStore.selectedArchitecture)" />
<TargetsUf2 v-if="deviceStore.selectedArchitecture === 'rp2040'" />
<TargetsNrf52 v-if="deviceStore.selectedArchitecture.startsWith('nrf52')" />
<TargetsEsp32 v-if="deviceStore.selectedArchitecture.startsWith('esp32')" />
</div>
<div id="erase-modal" tabindex="-1" aria-hidden="true"
Expand Down
2 changes: 1 addition & 1 deletion components/targets/EraseUf2.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<div class="p-4 mb-4 my-2 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-gray-800 dark:text-blue-400" role="alert">
<span class="font-medium">
<InformationCircleIcon class="h-4 w-4 inline" />
For firmware versions &lt; {{ deviceStore.enterDfuVersion }}, trigger DFU mode manually by {{ deviceStore.dfuStepAction }}
For firmware versions &lt; {{ deviceStore.enterDfuVersion }}, trigger DFU mode manually by {{ deviceStore.dfuStepAction }}.
<br />
After erasing flash, this operation will not be available again until new Meshtastic firmware is flashed on the device.
</span>
Expand Down
124 changes: 124 additions & 0 deletions components/targets/Nrf52.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<template>
<div class="relative w-full max-w-4xl max-h-full">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<FlashHeader />
<div class="p-4 md:p-5">
<ReleaseNotes />
<ol v-if="firmwareStore.canShowFlash" class="relative border-s border-gray-200 dark:border-gray-600 ms-3.5 mb-4 md:mb-5">
<li class="mb-10 ms-8">
<span class="absolute flex items-center justify-center w-6 h-6 bg-blue-100 rounded-full -start-3 ring-8 ring-white dark:ring-gray-900 dark:bg-blue-900">
1
</span>
<h3 class="flex items-start mb-1 text-lg font-semibold text-gray-900 dark:text-white">
Ensure device is plugged in via USB
</h3>
</li>
<li class="mb-10 ms-8">
<span class="absolute flex items-center justify-center w-6 h-6 bg-blue-100 rounded-full -start-3 ring-8 ring-white dark:ring-gray-900 dark:bg-blue-900">
2
</span>
<h3 class="flex items-start mb-1 text-lg font-semibold text-gray-900 dark:text-white">
Enter DFU Mode
</h3>
<div class="p-4 mb-4 my-2 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-gray-800 dark:text-blue-400" role="alert">
<span class="font-medium">
<!-- <InformationCircleIcon class="h-4 w-4 inline" /> -->
<button type="button"
class="inline-flex items-center py-2 px-3 text-sm font-medium focus:outline-none bg-meshtastic rounded-lg hover:bg-white focus:z-10 focus:ring-4 focus:ring-gray-200 text-black"
@click="deviceStore.enterDfuMode">
<FolderArrowDownIcon class="h-4 w-4 text-black" />
Enter DFU Mode
</button> or you can optionally enter DFU mode manually by {{ deviceStore.dfuStepAction }}.
</span>
</div>
</li>
<li class="ms-8">
<span class="absolute flex items-center justify-center w-6 h-6 bg-blue-100 rounded-full -start-3 ring-8 ring-white dark:ring-gray-900 dark:bg-blue-900">
3
</span>
<h3 class="mb-1 text-lg font-semibold text-gray-900 dark:text-white">
Flash firmware via DFU utility or
<a :href="downloadUf2FileUrl" v-if="firmwareStore.selectedFirmware?.id" class="inline-flex items-center py-2 px-3 text-sm font-medium focus:outline-none bg-meshtastic rounded-lg hover:bg-white focus:z-10 focus:ring-4 focus:ring-gray-200 text-black">
<ArrowDownTrayIcon class="h-4 w-4 text-black" />
Download UF2
</a>
<button @click="downloadUf2FileFs" v-else
class="inline-flex items-center py-2 px-3 text-sm font-medium focus:outline-none bg-meshtastic rounded-lg hover:bg-white focus:z-10 focus:ring-4 focus:ring-gray-200 text-black">
<ArrowDownTrayIcon class="h-4 w-4 text-black" />
Download UF2
</button>
onto the USB mass storage device
</h3>
<p>
This process could take a while.
</p>
</li>
</ol>
<div v-if="firmwareStore.canShowFlash">
<button v-if="showFlashButton"
class="text-black inline-flex w-full justify-center bg-meshtastic hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center" @click="flash">
Update via DFU Utility
</button>
<button v-if="firmwareStore.$state.flashPercentDone > 0 && !firmwareStore.$state.isFlashing"
class="mx-2 my-2 text-black inline-flex w-full justify-center bg-meshtastic hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center" @click="flash">
Start Over
</button>
<button v-if="firmwareStore.$state.flashPercentDone > 0 && !firmwareStore.$state.isFlashing"
class="mx-2 text-black inline-flex w-full justify-center bg-meshtastic hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center" @click="serialMonitor">
Open Serial Monitor
</button>
<div v-if="firmwareStore.$state.flashPercentDone > 0" class="mb-1 text-center font-medium text-white">Flashing {{ firmwareStore.percentDone }} complete</div>
<div class="w-fullrounded-full h-2.5 mb-4 bg-gray-700" v-if="firmwareStore.$state.flashPercentDone > 0">
<div class="bg-meshtastic h-2.5 rounded-full" :style=" { 'width': firmwareStore.percentDone }"></div>
</div>
</div>
</div>
</div>
</div>
</template>

<script lang="ts" setup>
import '@/node_modules/xterm/css/xterm.css';

import {
ArrowDownTrayIcon,
FolderArrowDownIcon,
} from '@heroicons/vue/24/solid';

import { useDeviceStore } from '../../stores/deviceStore';
import { useFirmwareStore } from '../../stores/firmwareStore';
import FlashHeader from './FlashHeader.vue';
import ReleaseNotes from './ReleaseNotes.vue';

const deviceStore = useDeviceStore();
const firmwareStore = useFirmwareStore();
const serialMonitorStore = useSerialMonitorStore();
const showFlashButton = computed(() => {
return !firmwareStore.$state.isFlashing && firmwareStore.$state.flashPercentDone < 1;
})

const downloadUf2FileFs = () => {
const searchRegex = new RegExp(`firmware-${deviceStore.$state.selectedTarget.platformioTarget}-.+.uf2`);
firmwareStore.downloadUf2FileSystem(searchRegex);
}

const downloadUf2FileUrl = computed(() => {
if (!firmwareStore.selectedFirmware?.id) return '';
const firmwareVersion = firmwareStore.selectedFirmware.id.replace('v', '')
const firmwareFile = `firmware-${deviceStore.$state.selectedTarget.platformioTarget}-${firmwareVersion}.uf2`
firmwareStore.trackDownload(deviceStore.$state.selectedTarget, false);
return firmwareStore.getReleaseFileUrl(firmwareFile);
})

const flash = async () => {
const otaZipFile = `firmware-${deviceStore.$state.selectedTarget.platformioTarget}-${firmwareStore.firmwareVersion}-ota.zip`
const port = await firmwareStore.flashNrf52(otaZipFile, deviceStore.$state.selectedTarget);
// await serialMonitorStore.monitorSerial(port);
}

const serialMonitor = async () => {
document.getElementById('flash-modal')?.click();
await serialMonitorStore.monitorSerial();
}

</script>
6 changes: 3 additions & 3 deletions components/targets/Uf2.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<div class="p-4 mb-4 my-2 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-gray-800 dark:text-blue-400" role="alert">
<span class="font-medium">
<InformationCircleIcon class="h-4 w-4 inline" />
For firmware versions &lt; {{ deviceStore.enterDfuVersion }}, trigger DFU mode manually by {{ deviceStore.dfuStepAction }}
For firmware versions &lt; {{ deviceStore.enterDfuVersion }}, trigger DFU mode manually by {{ deviceStore.dfuStepAction }}.
</span>
</div>
<button type="button"
Expand Down Expand Up @@ -61,8 +61,8 @@
</ol>
<div v-if="firmwareStore.canShowFlash">
<a :href="downloadUf2FileUrl" v-if="firmwareStore.selectedFirmware?.id"
class="text-black inline-flex w-full justify-center bg-meshtastic hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">
Download UF2
class="text-black inline-flex w-full justify-center bg-meshtastic hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">
Download UF2
</a>
<button @click="downloadUf2FileFs" v-else
class="text-black inline-flex w-full justify-center bg-meshtastic hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">
Expand Down
1 change: 0 additions & 1 deletion nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,5 @@ export default defineNuxtConfig({
}
}
},

compatibilityDate: '2024-09-03',
});
14 changes: 10 additions & 4 deletions stores/deviceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export const useDeviceStore = defineStore('device', {
},
dfuStepAction(): string {
if (this.isSelectedNrf) {
return 'double-clicking RST button.';
return 'double-clicking RST button';
} else {
return 'pressing and holding BOOTSEL button while plugging in USB cable.';
return 'pressing and holding BOOTSEL button while plugging in USB cable';
}
},
},
Expand Down Expand Up @@ -71,7 +71,8 @@ export const useDeviceStore = defineStore('device', {
});
return connection;
},
async enterDfuMode() {
async enterDfuModeLegacy() {
// Legacy API mode
const connection = await this.openDeviceConnection();
connection.events.onFromRadio.subscribe((packet: any) => {
if (packet?.payloadVariant?.case === "configCompleteId") {
Expand All @@ -80,6 +81,11 @@ export const useDeviceStore = defineStore('device', {
}
});
},
async enterDfuMode() {
const port: SerialPort = await navigator.serial.requestPort();
const nrfFlasher = new Nrf52DfuFlasher(port, () => {});
await nrfFlasher.enterDfuMode();
},
async baud1200() {
const port: SerialPort = await navigator.serial.requestPort();
await port.open({ baudRate: 1200 });
Expand All @@ -93,7 +99,7 @@ export const useDeviceStore = defineStore('device', {
}
return connection.disconnect();
});
await new Promise(_ => setTimeout(_, 4000));
await waitForMs(4000);
await connection.disconnect();
}
},
Expand Down
53 changes: 51 additions & 2 deletions stores/firmwareStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { mande } from 'mande';
import { defineStore } from 'pinia';
import type { Terminal } from 'xterm';

import { Nrf52DfuFlasher } from '@/utils/nrfUtil';
import { track } from '@vercel/analytics';
import { useSessionStorage } from '@vueuse/core';
import {
Expand All @@ -25,6 +26,7 @@ import {
} from '../types/api';
import { createUrl } from './store';


const previews = new Array<FirmwareResource>()// new Array<FirmwareResource>(currentPrerelease)

const firmwareApi = mande(createUrl('api/github/firmware/list'))
Expand Down Expand Up @@ -91,6 +93,32 @@ export const useFirmwareStore = defineStore('firmware', {
const baseUrl = getCorsFriendyReleaseUrl(this.selectedFirmware.zip_url);
return `${baseUrl}/${fileName}`;
},
async flashNrf52(fileName: string, selectedTarget: DeviceHardware): Promise<SerialPort> {
this.trackDownload(selectedTarget, false);
if (!this.port || !this.isConnected) {
this.port = await navigator.serial.requestPort({});
this.port.ondisconnect = () => {
this.isConnected = false;
};
await this.port.open({
baudRate: 115200,
});
}
this.isConnected = true;
this.isFlashing = true;
this.flashPercentDone = 1;
const nrfFlasher = new Nrf52DfuFlasher(this.port, (percent) => {
this.flashPercentDone = percent;
if (percent === 100) {
this.isFlashing = false;
console.log('Done flashing!');
}
});
const firmware = await this.fetchBlob(fileName);
await nrfFlasher.flash(firmware);
this.isFlashing = false;
return this.port;
},
async downloadUf2FileSystem(searchRegex: RegExp) {
if (!this.selectedFile) return;
const reader = new BlobReader(this.selectedFile);
Expand Down Expand Up @@ -155,7 +183,7 @@ export const useFirmwareStore = defineStore('firmware', {
},
async resetEsp32(transport: Transport) {
await transport.setRTS(true);
await new Promise((resolve) => setTimeout(resolve, 100));
await waitForMs(100);
await transport.setRTS(false);
},
trackDownload(selectedTarget: DeviceHardware, isCleanInstall: boolean) {
Expand Down Expand Up @@ -201,6 +229,27 @@ export const useFirmwareStore = defineStore('firmware', {
};
await this.startWrite(terminal, espLoader, transport, flashOptions);
},
async fetchBlob(fileName: string): Promise<Blob> {
if (this.selectedFirmware?.zip_url) {
const baseUrl = getCorsFriendyReleaseUrl(this.selectedFirmware!.zip_url!);
const response = await fetch(`${baseUrl}/${fileName}`);
return await response.blob();
} else if (this.selectedFile && this.isZipFile) {
const reader = new BlobReader(this.selectedFile!);
const zipReader = new ZipReader(reader);
const entries = await zipReader.getEntries()
console.log('Zip entries:', entries);
console.log('Looking for file matching pattern:', fileName);
const file = entries.find(entry => new RegExp(fileName).test(entry.filename))
if (file) {
console.log('Found file:', file.filename);
return await file.getData!(new BlobWriter());
}
} else if (this.selectedFile && !this.isZipFile) {
return this.selectedFile;
}
throw new Error('Cannot fetch blob without a file or firmware selected');
},
async fetchBinaryContent(fileName: string): Promise<string> {
if (this.selectedFirmware?.zip_url) {
const baseUrl = getCorsFriendyReleaseUrl(this.selectedFirmware.zip_url);
Expand Down Expand Up @@ -273,7 +322,7 @@ export const useFirmwareStore = defineStore('firmware', {
if (value) {
terminal.write(value);
}
await new Promise(resolve => setTimeout(resolve, 5));
await waitForMs(5);
}
},
},
Expand Down
12 changes: 8 additions & 4 deletions stores/serialMonitorStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,16 @@ export const useSerialMonitorStore = defineStore("serialMonitor", {
this.terminalBuffer[this.terminalBuffer.length - 1] = newLine;
}
}
await new Promise((resolve) => setTimeout(resolve, 5));
await waitForMs(1);
}
},
async monitorSerial() {
this.port = await navigator.serial.requestPort({});
await this.port.open({ baudRate: this.baudRate });
async monitorSerial(port: SerialPort | undefined = undefined) {
if (port) {
this.port = port;
} else {
this.port = await navigator.serial.requestPort({});
await this.port.open({ baudRate: this.baudRate });
}
this.isOpen = true;
this.isConnected = true;
this.port.ondisconnect = () => {
Expand Down
Loading