Skip to content

Commit 186facd

Browse files
committed
feat: Add web-based DFU firmware flasher for DUTLink devices
1 parent fd9041f commit 186facd

File tree

3 files changed

+697
-0
lines changed

3 files changed

+697
-0
lines changed

app.js

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
/**
2+
* DUTLink Firmware Flasher Web Application
3+
*/
4+
5+
let dfu = null;
6+
let releaseData = null;
7+
let bootloaderBinary = null;
8+
let applicationBinary = null;
9+
10+
// GitHub repository info
11+
const REPO_OWNER = "jumpstarter-dev";
12+
const REPO_NAME = "dutlink-firmware";
13+
const GITHUB_API_URL = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`;
14+
15+
// Initialize the application
16+
document.addEventListener('DOMContentLoaded', function() {
17+
checkWebUSBSupport();
18+
checkHTTPS();
19+
});
20+
21+
function checkWebUSBSupport() {
22+
if (!navigator.usb) {
23+
document.getElementById('webusb-support').classList.remove('hidden');
24+
return false;
25+
}
26+
return true;
27+
}
28+
29+
function checkHTTPS() {
30+
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
31+
document.getElementById('https-warning').classList.remove('hidden');
32+
return false;
33+
}
34+
return true;
35+
}
36+
37+
function addStatus(message, type = 'info') {
38+
const statusDiv = document.getElementById('status-messages');
39+
const statusElement = document.createElement('div');
40+
statusElement.className = `status ${type}`;
41+
statusElement.textContent = message;
42+
statusDiv.appendChild(statusElement);
43+
44+
// Auto-remove after 10 seconds for non-error messages
45+
if (type !== 'error') {
46+
setTimeout(() => {
47+
if (statusElement.parentNode) {
48+
statusElement.parentNode.removeChild(statusElement);
49+
}
50+
}, 10000);
51+
}
52+
53+
// Scroll to bottom
54+
statusDiv.scrollTop = statusDiv.scrollHeight;
55+
}
56+
57+
function addLog(message) {
58+
const logContainer = document.getElementById('log-container');
59+
logContainer.classList.remove('hidden');
60+
logContainer.textContent += new Date().toLocaleTimeString() + ': ' + message + '\n';
61+
logContainer.scrollTop = logContainer.scrollHeight;
62+
}
63+
64+
function updateProgress(current, total) {
65+
const progressContainer = document.getElementById('progress-container');
66+
const progressBar = document.getElementById('progress-bar');
67+
68+
if (total > 0) {
69+
progressContainer.classList.remove('hidden');
70+
const percentage = (current / total) * 100;
71+
progressBar.style.width = percentage + '%';
72+
} else {
73+
progressContainer.classList.add('hidden');
74+
}
75+
}
76+
77+
async function checkLatestRelease() {
78+
const button = document.getElementById('check-release-btn');
79+
button.disabled = true;
80+
button.textContent = '🔄 Checking...';
81+
82+
try {
83+
addStatus('Fetching latest release information...', 'info');
84+
addLog('Fetching release data from GitHub API');
85+
86+
const response = await fetch(GITHUB_API_URL);
87+
if (!response.ok) {
88+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
89+
}
90+
91+
releaseData = await response.json();
92+
93+
// Find bootloader and application assets
94+
const bootloaderAsset = releaseData.assets.find(asset =>
95+
asset.name.includes('dfu-bootloader') && asset.name.endsWith('.bin')
96+
);
97+
const applicationAsset = releaseData.assets.find(asset =>
98+
asset.name.includes('jumpstarter') && asset.name.endsWith('.bin')
99+
);
100+
101+
if (!bootloaderAsset || !applicationAsset) {
102+
throw new Error('Could not find required binary assets in release');
103+
}
104+
105+
// Update UI with release info
106+
document.getElementById('release-version').textContent = releaseData.tag_name;
107+
document.getElementById('bootloader-size').textContent = formatBytes(bootloaderAsset.size);
108+
document.getElementById('application-size').textContent = formatBytes(applicationAsset.size);
109+
document.getElementById('release-info').classList.remove('hidden');
110+
111+
// Download binaries
112+
addStatus('Downloading firmware binaries...', 'info');
113+
addLog('Downloading bootloader binary');
114+
bootloaderBinary = await downloadBinary(bootloaderAsset.browser_download_url);
115+
116+
addLog('Downloading application binary');
117+
applicationBinary = await downloadBinary(applicationAsset.browser_download_url);
118+
119+
addStatus(`✅ Ready to flash firmware ${releaseData.tag_name}`, 'success');
120+
addLog('Binaries downloaded successfully');
121+
122+
// Enable connect button
123+
document.getElementById('connect-btn').disabled = false;
124+
125+
} catch (error) {
126+
addStatus(`❌ Failed to fetch release: ${error.message}`, 'error');
127+
addLog(`Error: ${error.message}`);
128+
} finally {
129+
button.disabled = false;
130+
button.textContent = '📡 Check Latest Release';
131+
}
132+
}
133+
134+
async function downloadBinary(url) {
135+
const response = await fetch(url);
136+
if (!response.ok) {
137+
throw new Error(`Failed to download: HTTP ${response.status}`);
138+
}
139+
140+
const arrayBuffer = await response.arrayBuffer();
141+
return new Uint8Array(arrayBuffer);
142+
}
143+
144+
function formatBytes(bytes) {
145+
if (bytes === 0) return '0 Bytes';
146+
const k = 1024;
147+
const sizes = ['Bytes', 'KB', 'MB'];
148+
const i = Math.floor(Math.log(bytes) / Math.log(k));
149+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
150+
}
151+
152+
async function connectDevice() {
153+
const button = document.getElementById('connect-btn');
154+
button.disabled = true;
155+
button.textContent = '🔄 Connecting...';
156+
157+
try {
158+
addStatus('Connecting to DFU device...', 'info');
159+
addLog('Requesting USB device access');
160+
161+
dfu = new WebDFU();
162+
await dfu.connect();
163+
164+
addStatus('✅ Connected to DFU device', 'success');
165+
addLog(`Connected to device: ${dfu.device.productName || 'Unknown'}`);
166+
167+
// Enable flash button
168+
document.getElementById('flash-btn').disabled = false;
169+
button.textContent = '✅ Connected';
170+
171+
} catch (error) {
172+
addStatus(`❌ Failed to connect: ${error.message}`, 'error');
173+
addLog(`Connection error: ${error.message}`);
174+
button.disabled = false;
175+
button.textContent = '🔌 Connect to Device';
176+
dfu = null;
177+
}
178+
}
179+
180+
async function flashFirmware() {
181+
if (!dfu || !bootloaderBinary || !applicationBinary) {
182+
addStatus('❌ Not ready to flash. Please check release and connect device first.', 'error');
183+
return;
184+
}
185+
186+
const button = document.getElementById('flash-btn');
187+
button.disabled = true;
188+
button.textContent = '⚡ Flashing...';
189+
190+
try {
191+
// Set up progress callback
192+
dfu.onProgress = updateProgress;
193+
194+
addStatus('🔄 Starting firmware flash process...', 'info');
195+
addLog('Starting flash process');
196+
197+
// Flash bootloader first
198+
addStatus('📝 Flashing bootloader...', 'info');
199+
addLog('Flashing bootloader to 0x08000000');
200+
await dfu.download(bootloaderBinary, 0x08000000);
201+
202+
addStatus('✅ Bootloader flashed successfully', 'success');
203+
addLog('Bootloader flash completed');
204+
205+
// Detach to reset device
206+
addStatus('🔄 Resetting device...', 'info');
207+
addLog('Detaching device to trigger reset');
208+
await dfu.detach();
209+
await dfu.disconnect();
210+
211+
// Wait for device to reset and enter custom bootloader
212+
addStatus('⏳ Waiting for device to enter custom bootloader...', 'warning');
213+
addLog('Waiting 3 seconds for device reset');
214+
await new Promise(resolve => setTimeout(resolve, 3000));
215+
216+
// Reconnect to custom bootloader
217+
addStatus('🔄 Connecting to custom bootloader...', 'info');
218+
addLog('Attempting to connect to custom bootloader');
219+
220+
dfu = new WebDFU();
221+
await dfu.connect([{ vendorId: 0x2b23, productId: 0x1012 }]);
222+
dfu.onProgress = updateProgress;
223+
224+
addStatus('✅ Connected to custom bootloader', 'success');
225+
addLog('Connected to custom bootloader');
226+
227+
// Flash application
228+
addStatus('📝 Flashing application...', 'info');
229+
addLog('Flashing application to 0x08010000');
230+
await dfu.download(applicationBinary, 0x08010000);
231+
232+
addStatus('✅ Application flashed successfully', 'success');
233+
addLog('Application flash completed');
234+
235+
// Final detach
236+
addLog('Final device detach');
237+
await dfu.detach();
238+
await dfu.disconnect();
239+
240+
updateProgress(0, 0); // Hide progress bar
241+
242+
addStatus('🎉 FIRMWARE FLASH COMPLETE! 🎉', 'success');
243+
addStatus(`Successfully flashed DUTLink firmware ${releaseData.tag_name}`, 'success');
244+
addLog('Flash process completed successfully');
245+
246+
// Reset UI
247+
document.getElementById('connect-btn').disabled = true;
248+
document.getElementById('connect-btn').textContent = '🔌 Connect to Device';
249+
250+
} catch (error) {
251+
addStatus(`❌ Flash failed: ${error.message}`, 'error');
252+
addLog(`Flash error: ${error.message}`);
253+
254+
if (dfu) {
255+
try {
256+
await dfu.disconnect();
257+
} catch (e) {
258+
console.error('Error disconnecting:', e);
259+
}
260+
}
261+
262+
// Reset UI
263+
document.getElementById('connect-btn').disabled = false;
264+
document.getElementById('connect-btn').textContent = '🔌 Connect to Device';
265+
266+
} finally {
267+
button.disabled = false;
268+
button.textContent = '⚡ Flash Firmware';
269+
updateProgress(0, 0);
270+
dfu = null;
271+
}
272+
}
273+
274+
// Handle device disconnection
275+
navigator.usb.addEventListener('disconnect', event => {
276+
if (dfu && dfu.device === event.device) {
277+
addStatus('📱 Device disconnected', 'warning');
278+
addLog('USB device disconnected');
279+
dfu = null;
280+
281+
// Reset UI
282+
document.getElementById('connect-btn').disabled = false;
283+
document.getElementById('connect-btn').textContent = '🔌 Connect to Device';
284+
document.getElementById('flash-btn').disabled = true;
285+
}
286+
});

0 commit comments

Comments
 (0)