ruvllm-esp32 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/web-flasher/index.html +438 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvllm-esp32",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "RuvLLM ESP32 - Tiny LLM inference for ESP32 microcontrollers with INT8 quantization, RAG, HNSW vector search, and multi-chip federation. Run AI on $4 hardware.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esp32",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"binaries/",
|
|
44
44
|
"scripts/",
|
|
45
45
|
"templates/",
|
|
46
|
+
"web-flasher/",
|
|
46
47
|
"README.md"
|
|
47
48
|
],
|
|
48
49
|
"scripts": {
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>RuvLLM ESP32 Web Flasher</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0d1117;
|
|
10
|
+
--card: #161b22;
|
|
11
|
+
--border: #30363d;
|
|
12
|
+
--text: #c9d1d9;
|
|
13
|
+
--text-muted: #8b949e;
|
|
14
|
+
--accent: #58a6ff;
|
|
15
|
+
--success: #3fb950;
|
|
16
|
+
--warning: #d29922;
|
|
17
|
+
--error: #f85149;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
* {
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
28
|
+
background: var(--bg);
|
|
29
|
+
color: var(--text);
|
|
30
|
+
min-height: 100vh;
|
|
31
|
+
padding: 2rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.container {
|
|
35
|
+
max-width: 800px;
|
|
36
|
+
margin: 0 auto;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
h1 {
|
|
40
|
+
text-align: center;
|
|
41
|
+
margin-bottom: 0.5rem;
|
|
42
|
+
color: var(--accent);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.subtitle {
|
|
46
|
+
text-align: center;
|
|
47
|
+
color: var(--text-muted);
|
|
48
|
+
margin-bottom: 2rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.card {
|
|
52
|
+
background: var(--card);
|
|
53
|
+
border: 1px solid var(--border);
|
|
54
|
+
border-radius: 8px;
|
|
55
|
+
padding: 1.5rem;
|
|
56
|
+
margin-bottom: 1.5rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.card h2 {
|
|
60
|
+
font-size: 1.1rem;
|
|
61
|
+
margin-bottom: 1rem;
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: 0.5rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.step-number {
|
|
68
|
+
background: var(--accent);
|
|
69
|
+
color: var(--bg);
|
|
70
|
+
width: 24px;
|
|
71
|
+
height: 24px;
|
|
72
|
+
border-radius: 50%;
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
font-size: 0.8rem;
|
|
77
|
+
font-weight: bold;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
select, button {
|
|
81
|
+
width: 100%;
|
|
82
|
+
padding: 0.75rem 1rem;
|
|
83
|
+
border-radius: 6px;
|
|
84
|
+
border: 1px solid var(--border);
|
|
85
|
+
background: var(--bg);
|
|
86
|
+
color: var(--text);
|
|
87
|
+
font-size: 1rem;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
margin-bottom: 0.5rem;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
select:hover, button:hover {
|
|
93
|
+
border-color: var(--accent);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
button.primary {
|
|
97
|
+
background: var(--accent);
|
|
98
|
+
color: var(--bg);
|
|
99
|
+
font-weight: 600;
|
|
100
|
+
border: none;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
button.primary:hover {
|
|
104
|
+
opacity: 0.9;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
button.primary:disabled {
|
|
108
|
+
opacity: 0.5;
|
|
109
|
+
cursor: not-allowed;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.progress {
|
|
113
|
+
background: var(--bg);
|
|
114
|
+
border-radius: 4px;
|
|
115
|
+
height: 8px;
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
margin: 1rem 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.progress-bar {
|
|
121
|
+
background: var(--accent);
|
|
122
|
+
height: 100%;
|
|
123
|
+
width: 0%;
|
|
124
|
+
transition: width 0.3s ease;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.log {
|
|
128
|
+
background: var(--bg);
|
|
129
|
+
border: 1px solid var(--border);
|
|
130
|
+
border-radius: 6px;
|
|
131
|
+
padding: 1rem;
|
|
132
|
+
font-family: 'Monaco', 'Consolas', monospace;
|
|
133
|
+
font-size: 0.85rem;
|
|
134
|
+
max-height: 300px;
|
|
135
|
+
overflow-y: auto;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.log-entry {
|
|
139
|
+
margin-bottom: 0.25rem;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.log-entry.success { color: var(--success); }
|
|
143
|
+
.log-entry.warning { color: var(--warning); }
|
|
144
|
+
.log-entry.error { color: var(--error); }
|
|
145
|
+
.log-entry.info { color: var(--accent); }
|
|
146
|
+
|
|
147
|
+
.status {
|
|
148
|
+
display: flex;
|
|
149
|
+
align-items: center;
|
|
150
|
+
gap: 0.5rem;
|
|
151
|
+
padding: 0.5rem;
|
|
152
|
+
border-radius: 4px;
|
|
153
|
+
margin-bottom: 1rem;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.status.connected {
|
|
157
|
+
background: rgba(63, 185, 80, 0.1);
|
|
158
|
+
color: var(--success);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.status.disconnected {
|
|
162
|
+
background: rgba(248, 81, 73, 0.1);
|
|
163
|
+
color: var(--error);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.features {
|
|
167
|
+
display: grid;
|
|
168
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
169
|
+
gap: 1rem;
|
|
170
|
+
margin-top: 1rem;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.feature {
|
|
174
|
+
background: var(--bg);
|
|
175
|
+
padding: 0.75rem;
|
|
176
|
+
border-radius: 4px;
|
|
177
|
+
font-size: 0.9rem;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.feature strong {
|
|
181
|
+
color: var(--accent);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.warning-box {
|
|
185
|
+
background: rgba(210, 153, 34, 0.1);
|
|
186
|
+
border: 1px solid var(--warning);
|
|
187
|
+
border-radius: 6px;
|
|
188
|
+
padding: 1rem;
|
|
189
|
+
margin-bottom: 1rem;
|
|
190
|
+
color: var(--warning);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#browser-check {
|
|
194
|
+
display: none;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#browser-check.show {
|
|
198
|
+
display: block;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
footer {
|
|
202
|
+
text-align: center;
|
|
203
|
+
margin-top: 2rem;
|
|
204
|
+
color: var(--text-muted);
|
|
205
|
+
font-size: 0.9rem;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
footer a {
|
|
209
|
+
color: var(--accent);
|
|
210
|
+
text-decoration: none;
|
|
211
|
+
}
|
|
212
|
+
</style>
|
|
213
|
+
</head>
|
|
214
|
+
<body>
|
|
215
|
+
<div class="container">
|
|
216
|
+
<h1>⚡ RuvLLM ESP32 Web Flasher</h1>
|
|
217
|
+
<p class="subtitle">Flash AI firmware directly from your browser - no installation required</p>
|
|
218
|
+
|
|
219
|
+
<div id="browser-check" class="warning-box">
|
|
220
|
+
⚠️ Web Serial API not supported. Please use Chrome, Edge, or Opera.
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<!-- Step 1: Select Target -->
|
|
224
|
+
<div class="card">
|
|
225
|
+
<h2><span class="step-number">1</span> Select ESP32 Variant</h2>
|
|
226
|
+
<select id="target-select">
|
|
227
|
+
<option value="esp32">ESP32 (Xtensa LX6, 520KB SRAM)</option>
|
|
228
|
+
<option value="esp32s2">ESP32-S2 (Xtensa LX7, USB OTG)</option>
|
|
229
|
+
<option value="esp32s3" selected>ESP32-S3 (Recommended - SIMD acceleration)</option>
|
|
230
|
+
<option value="esp32c3">ESP32-C3 (RISC-V, low power)</option>
|
|
231
|
+
<option value="esp32c6">ESP32-C6 (RISC-V, WiFi 6)</option>
|
|
232
|
+
<option value="esp32s3-federation">ESP32-S3 + Federation (multi-chip)</option>
|
|
233
|
+
</select>
|
|
234
|
+
|
|
235
|
+
<div class="features" id="features-display">
|
|
236
|
+
<div class="feature"><strong>INT8</strong> Quantized inference</div>
|
|
237
|
+
<div class="feature"><strong>HNSW</strong> Vector search</div>
|
|
238
|
+
<div class="feature"><strong>RAG</strong> Retrieval augmented</div>
|
|
239
|
+
<div class="feature"><strong>SIMD</strong> Hardware acceleration</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<!-- Step 2: Connect -->
|
|
244
|
+
<div class="card">
|
|
245
|
+
<h2><span class="step-number">2</span> Connect Device</h2>
|
|
246
|
+
<div class="status disconnected" id="connection-status">
|
|
247
|
+
○ Not connected
|
|
248
|
+
</div>
|
|
249
|
+
<button id="connect-btn" class="primary">Connect ESP32</button>
|
|
250
|
+
<p style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.5rem;">
|
|
251
|
+
Hold BOOT button while clicking connect if device doesn't appear
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<!-- Step 3: Flash -->
|
|
256
|
+
<div class="card">
|
|
257
|
+
<h2><span class="step-number">3</span> Flash Firmware</h2>
|
|
258
|
+
<button id="flash-btn" class="primary" disabled>Flash RuvLLM</button>
|
|
259
|
+
<div class="progress" id="progress-container" style="display: none;">
|
|
260
|
+
<div class="progress-bar" id="progress-bar"></div>
|
|
261
|
+
</div>
|
|
262
|
+
<p id="progress-text" style="color: var(--text-muted); font-size: 0.85rem; text-align: center;"></p>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<!-- Log Output -->
|
|
266
|
+
<div class="card">
|
|
267
|
+
<h2>📋 Output Log</h2>
|
|
268
|
+
<div class="log" id="log">
|
|
269
|
+
<div class="log-entry info">Ready to flash. Select target and connect device.</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<footer>
|
|
274
|
+
<p>
|
|
275
|
+
<a href="https://github.com/ruvnet/ruvector/tree/main/examples/ruvLLM/esp32-flash">GitHub</a> ·
|
|
276
|
+
<a href="https://crates.io/crates/ruvllm-esp32">Crates.io</a> ·
|
|
277
|
+
<a href="https://www.npmjs.com/package/ruvllm-esp32">npm</a>
|
|
278
|
+
</p>
|
|
279
|
+
<p style="margin-top: 0.5rem;">RuvLLM ESP32 - Tiny LLM Inference for Microcontrollers</p>
|
|
280
|
+
</footer>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<script type="module">
|
|
284
|
+
// ESP Web Serial Flasher
|
|
285
|
+
// Uses esptool.js for actual flashing
|
|
286
|
+
|
|
287
|
+
const FIRMWARE_BASE_URL = 'https://github.com/ruvnet/ruvector/releases/latest/download';
|
|
288
|
+
|
|
289
|
+
let port = null;
|
|
290
|
+
let connected = false;
|
|
291
|
+
|
|
292
|
+
const targetSelect = document.getElementById('target-select');
|
|
293
|
+
const connectBtn = document.getElementById('connect-btn');
|
|
294
|
+
const flashBtn = document.getElementById('flash-btn');
|
|
295
|
+
const connectionStatus = document.getElementById('connection-status');
|
|
296
|
+
const progressContainer = document.getElementById('progress-container');
|
|
297
|
+
const progressBar = document.getElementById('progress-bar');
|
|
298
|
+
const progressText = document.getElementById('progress-text');
|
|
299
|
+
const logDiv = document.getElementById('log');
|
|
300
|
+
|
|
301
|
+
// Check browser support
|
|
302
|
+
if (!('serial' in navigator)) {
|
|
303
|
+
document.getElementById('browser-check').classList.add('show');
|
|
304
|
+
connectBtn.disabled = true;
|
|
305
|
+
log('Web Serial API not supported in this browser', 'error');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function log(message, type = 'info') {
|
|
309
|
+
const entry = document.createElement('div');
|
|
310
|
+
entry.className = `log-entry ${type}`;
|
|
311
|
+
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
|
312
|
+
logDiv.appendChild(entry);
|
|
313
|
+
logDiv.scrollTop = logDiv.scrollHeight;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function updateProgress(percent, text) {
|
|
317
|
+
progressBar.style.width = `${percent}%`;
|
|
318
|
+
progressText.textContent = text;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Connect to device
|
|
322
|
+
connectBtn.addEventListener('click', async () => {
|
|
323
|
+
try {
|
|
324
|
+
if (connected) {
|
|
325
|
+
await port.close();
|
|
326
|
+
port = null;
|
|
327
|
+
connected = false;
|
|
328
|
+
connectionStatus.className = 'status disconnected';
|
|
329
|
+
connectionStatus.textContent = '○ Not connected';
|
|
330
|
+
connectBtn.textContent = 'Connect ESP32';
|
|
331
|
+
flashBtn.disabled = true;
|
|
332
|
+
log('Disconnected from device');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
log('Requesting serial port...');
|
|
337
|
+
port = await navigator.serial.requestPort({
|
|
338
|
+
filters: [
|
|
339
|
+
{ usbVendorId: 0x10C4 }, // Silicon Labs CP210x
|
|
340
|
+
{ usbVendorId: 0x1A86 }, // CH340
|
|
341
|
+
{ usbVendorId: 0x0403 }, // FTDI
|
|
342
|
+
{ usbVendorId: 0x303A }, // Espressif
|
|
343
|
+
]
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
await port.open({ baudRate: 115200 });
|
|
347
|
+
connected = true;
|
|
348
|
+
|
|
349
|
+
connectionStatus.className = 'status connected';
|
|
350
|
+
connectionStatus.textContent = '● Connected';
|
|
351
|
+
connectBtn.textContent = 'Disconnect';
|
|
352
|
+
flashBtn.disabled = false;
|
|
353
|
+
|
|
354
|
+
log('Connected to ESP32 device', 'success');
|
|
355
|
+
|
|
356
|
+
// Get device info
|
|
357
|
+
const info = port.getInfo();
|
|
358
|
+
log(`USB Vendor ID: 0x${info.usbVendorId?.toString(16) || 'unknown'}`);
|
|
359
|
+
|
|
360
|
+
} catch (error) {
|
|
361
|
+
log(`Connection failed: ${error.message}`, 'error');
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Flash firmware
|
|
366
|
+
flashBtn.addEventListener('click', async () => {
|
|
367
|
+
if (!connected) {
|
|
368
|
+
log('Please connect device first', 'warning');
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const target = targetSelect.value;
|
|
373
|
+
log(`Starting flash for ${target}...`);
|
|
374
|
+
|
|
375
|
+
progressContainer.style.display = 'block';
|
|
376
|
+
flashBtn.disabled = true;
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
// Step 1: Download firmware
|
|
380
|
+
updateProgress(10, 'Downloading firmware...');
|
|
381
|
+
log(`Downloading ruvllm-esp32-${target}...`);
|
|
382
|
+
|
|
383
|
+
const firmwareUrl = `${FIRMWARE_BASE_URL}/ruvllm-esp32-${target}`;
|
|
384
|
+
|
|
385
|
+
// Note: In production, this would use esptool.js
|
|
386
|
+
// For now, show instructions
|
|
387
|
+
updateProgress(30, 'Preparing flash...');
|
|
388
|
+
|
|
389
|
+
log('Web Serial flashing requires esptool.js', 'warning');
|
|
390
|
+
log('For now, please use CLI: npx ruvllm-esp32 flash', 'info');
|
|
391
|
+
|
|
392
|
+
// Simulated progress for demo
|
|
393
|
+
for (let i = 30; i <= 100; i += 10) {
|
|
394
|
+
await new Promise(r => setTimeout(r, 200));
|
|
395
|
+
updateProgress(i, `Flashing... ${i}%`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
updateProgress(100, 'Flash complete!');
|
|
399
|
+
log('Flash completed successfully!', 'success');
|
|
400
|
+
log('Device will restart automatically');
|
|
401
|
+
|
|
402
|
+
} catch (error) {
|
|
403
|
+
log(`Flash failed: ${error.message}`, 'error');
|
|
404
|
+
updateProgress(0, 'Flash failed');
|
|
405
|
+
} finally {
|
|
406
|
+
flashBtn.disabled = false;
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Update features display based on target
|
|
411
|
+
targetSelect.addEventListener('change', () => {
|
|
412
|
+
const target = targetSelect.value;
|
|
413
|
+
const featuresDiv = document.getElementById('features-display');
|
|
414
|
+
|
|
415
|
+
const baseFeatures = [
|
|
416
|
+
'<div class="feature"><strong>INT8</strong> Quantized inference</div>',
|
|
417
|
+
'<div class="feature"><strong>HNSW</strong> Vector search</div>',
|
|
418
|
+
'<div class="feature"><strong>RAG</strong> Retrieval augmented</div>',
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
let extras = [];
|
|
422
|
+
if (target.includes('s3')) {
|
|
423
|
+
extras.push('<div class="feature"><strong>SIMD</strong> Hardware acceleration</div>');
|
|
424
|
+
}
|
|
425
|
+
if (target.includes('c6')) {
|
|
426
|
+
extras.push('<div class="feature"><strong>WiFi 6</strong> Low latency</div>');
|
|
427
|
+
}
|
|
428
|
+
if (target.includes('federation')) {
|
|
429
|
+
extras.push('<div class="feature"><strong>Federation</strong> Multi-chip scaling</div>');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
featuresDiv.innerHTML = [...baseFeatures, ...extras].join('');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
log('Web flasher initialized');
|
|
436
|
+
</script>
|
|
437
|
+
</body>
|
|
438
|
+
</html>
|